diff options
560 files changed, 15440 insertions, 6316 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index a16aa2dea25b..7a1add3eb57e 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -98,6 +98,7 @@ aconfig_declarations_group { "framework-jobscheduler-job.flags-aconfig-java", "framework_graphics_flags_java_lib", "hwui_flags_java_lib", + "libcore_exported_aconfig_flags_lib", "power_flags_lib", "sdk_sandbox_flags_lib", "surfaceflinger_flags_java_lib", @@ -140,6 +141,14 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Core Libraries / libcore +java_aconfig_library { + name: "libcore_exported_aconfig_flags_lib", + aconfig_declarations: "libcore-aconfig-flags", + mode: "exported", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Telecom java_aconfig_library { name: "telecom_flags_core_java_lib", @@ -233,6 +242,12 @@ cc_aconfig_library { aconfig_declarations: "com.android.text.flags-aconfig", } +rust_aconfig_library { + name: "libandroid_text_flags_rust", + crate_name: "android_text_flags", + aconfig_declarations: "com.android.text.flags-aconfig", +} + // Location aconfig_declarations { name: "android.location.flags-aconfig", @@ -363,6 +378,7 @@ java_aconfig_library { min_sdk_version: "30", apex_available: [ "//apex_available:platform", + "com.android.btservices", "com.android.mediaprovider", "com.android.permission", ], @@ -403,17 +419,6 @@ java_aconfig_library { cc_aconfig_library { name: "android.companion.virtualdevice.flags-aconfig-cc", aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", -} - -cc_aconfig_library { - name: "android.companion.virtualdevice.flags-aconfig-cc-host", - aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", - host_supported: true, -} - -cc_aconfig_library { - name: "android.companion.virtualdevice.flags-aconfig-cc-test", - aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", host_supported: true, mode: "test", } @@ -1490,6 +1495,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "backstage_power_flags_lib-host", + aconfig_declarations: "backstage_power_flags", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Dropbox data aconfig_declarations { name: "dropbox_flags", diff --git a/Android.bp b/Android.bp index f0aa62cc37ae..eabd9c7565da 100644 --- a/Android.bp +++ b/Android.bp @@ -417,7 +417,6 @@ java_defaults { "modules-utils-fastxmlserializer", "modules-utils-preconditions", "modules-utils-statemachine", - "modules-utils-synchronous-result-receiver", "modules-utils-os", "modules-utils-uieventlogger-interface", "framework-permission-aidl-java", diff --git a/apct-tests/perftests/multiuser/Android.bp b/apct-tests/perftests/multiuser/Android.bp index 1653edc77de9..856dba3f804c 100644 --- a/apct-tests/perftests/multiuser/Android.bp +++ b/apct-tests/perftests/multiuser/Android.bp @@ -38,3 +38,10 @@ android_test { ], certificate: "platform", } + +filegroup { + name: "multi_user_trace_config", + srcs: [ + "trace_configs/trace_config_multi_user.textproto", + ], +} diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java index dc5e3414a819..93904a773ed5 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java @@ -20,6 +20,7 @@ package com.android.server.alarm; import android.annotation.Nullable; import android.os.Environment; import android.os.SystemClock; +import android.os.UserHandle; import android.util.AtomicFile; import android.util.IndentingPrintWriter; import android.util.Pair; @@ -113,15 +114,18 @@ public class UserWakeupStore { } /** - * Add user wakeup for the alarm. + * Add user wakeup for the alarm if needed. * @param userId Id of the user that scheduled alarm. * @param alarmTime time when alarm is expected to trigger. */ public void addUserWakeup(int userId, long alarmTime) { - synchronized (mUserWakeupLock) { - mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset()); + // SYSTEM user is always running, so no need to schedule wakeup for it. + if (userId != UserHandle.USER_SYSTEM) { + synchronized (mUserWakeupLock) { + mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset()); + } + updateUserListFile(); } - updateUserListFile(); } /** diff --git a/api/Android.bp b/api/Android.bp index 6a04f0d76bf9..d931df165a8f 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -29,12 +29,14 @@ bootstrap_go_package { pkgPath: "android/soong/api", deps: [ "blueprint", + "blueprint-proptools", "soong", "soong-android", "soong-genrule", "soong-java", ], srcs: ["api.go"], + testSrcs: ["api_test.go"], pluginFor: ["soong_build"], } @@ -60,40 +62,8 @@ metalava_cmd = "$(location metalava)" metalava_cmd += " -J--add-opens=java.base/java.util=ALL-UNNAMED " metalava_cmd += " --quiet " -soong_config_module_type { - name: "enable_crashrecovery_module", - module_type: "combined_apis_defaults", - config_namespace: "ANDROID", - bool_variables: ["release_crashrecovery_module"], - properties: [ - "bootclasspath", - "system_server_classpath", - ], -} - -soong_config_bool_variable { - name: "release_crashrecovery_module", -} - -enable_crashrecovery_module { - name: "crashrecovery_module_defaults", - soong_config_variables: { - release_crashrecovery_module: { - bootclasspath: [ - "framework-crashrecovery", - ], - system_server_classpath: [ - "service-crashrecovery", - ], - }, - }, -} - combined_apis { name: "frameworks-base-api", - defaults: [ - "crashrecovery_module_defaults", - ], bootclasspath: [ "android.net.ipsec.ike", "art.module.public.api", @@ -126,7 +96,12 @@ combined_apis { "framework-virtualization", "framework-wifi", "i18n.module.public.api", - ], + ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { + "true": [ + "framework-crashrecovery", + ], + default: [], + }), system_server_classpath: [ "service-art", "service-configinfrastructure", @@ -135,7 +110,12 @@ combined_apis { "service-permission", "service-rkp", "service-sdksandbox", - ], + ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { + "true": [ + "service-crashrecovery", + ], + default: [], + }), } genrule { diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 12820f9ff277..8dfddf0e13c8 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -1345,4 +1345,5 @@ java_library { ":hwbinder-stubs-docs", ], visibility: ["//visibility:public"], + is_stubs_module: true, } diff --git a/api/api.go b/api/api.go index d4db49e90a01..b6b1a7e44510 100644 --- a/api/api.go +++ b/api/api.go @@ -54,16 +54,15 @@ var non_updatable_modules = []string{virtualization, location} // The properties of the combined_apis module type. type CombinedApisProperties struct { // Module libraries in the bootclasspath - Bootclasspath []string + Bootclasspath proptools.Configurable[[]string] // Module libraries on the bootclasspath if include_nonpublic_framework_api is true. Conditional_bootclasspath []string // Module libraries in system server - System_server_classpath []string + System_server_classpath proptools.Configurable[[]string] } type CombinedApis struct { android.ModuleBase - android.DefaultableModuleBase properties CombinedApisProperties } @@ -74,34 +73,41 @@ func init() { func registerBuildComponents(ctx android.RegistrationContext) { ctx.RegisterModuleType("combined_apis", combinedApisModuleFactory) - ctx.RegisterModuleType("combined_apis_defaults", CombinedApisModuleDefaultsFactory) } var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents) -func (a *CombinedApis) apiFingerprintStubDeps() []string { +func (a *CombinedApis) bootclasspath(ctx android.ConfigAndErrorContext) []string { + return a.properties.Bootclasspath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil) +} + +func (a *CombinedApis) systemServerClasspath(ctx android.ConfigAndErrorContext) []string { + return a.properties.System_server_classpath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil) +} + +func (a *CombinedApis) apiFingerprintStubDeps(ctx android.BottomUpMutatorContext) []string { ret := []string{} ret = append( ret, - transformArray(a.properties.Bootclasspath, "", ".stubs")..., + transformArray(a.bootclasspath(ctx), "", ".stubs")..., ) ret = append( ret, - transformArray(a.properties.Bootclasspath, "", ".stubs.system")..., + transformArray(a.bootclasspath(ctx), "", ".stubs.system")..., ) ret = append( ret, - transformArray(a.properties.Bootclasspath, "", ".stubs.module_lib")..., + transformArray(a.bootclasspath(ctx), "", ".stubs.module_lib")..., ) ret = append( ret, - transformArray(a.properties.System_server_classpath, "", ".stubs.system_server")..., + transformArray(a.systemServerClasspath(ctx), "", ".stubs.system_server")..., ) return ret } func (a *CombinedApis) DepsMutator(ctx android.BottomUpMutatorContext) { - ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps()...) + ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps(ctx)...) } func (a *CombinedApis) GenerateAndroidBuildActions(ctx android.ModuleContext) { @@ -532,8 +538,8 @@ func createFullExportableApiLibraries(ctx android.LoadHookContext) { } func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { - bootclasspath := a.properties.Bootclasspath - system_server_classpath := a.properties.System_server_classpath + bootclasspath := a.bootclasspath(ctx) + system_server_classpath := a.systemServerClasspath(ctx) if ctx.Config().VendorConfig("ANDROID").Bool("include_nonpublic_framework_api") { bootclasspath = append(bootclasspath, a.properties.Conditional_bootclasspath...) sort.Strings(bootclasspath) @@ -568,7 +574,6 @@ func combinedApisModuleFactory() android.Module { module := &CombinedApis{} module.AddProperties(&module.properties) android.InitAndroidModule(module) - android.InitDefaultableModule(module) android.AddLoadHook(module, func(ctx android.LoadHookContext) { module.createInternalModules(ctx) }) return module } @@ -605,16 +610,3 @@ func remove(s []string, v string) []string { } return s2 } - -// Defaults -type CombinedApisModuleDefaults struct { - android.ModuleBase - android.DefaultsModuleBase -} - -func CombinedApisModuleDefaultsFactory() android.Module { - module := &CombinedApisModuleDefaults{} - module.AddProperties(&CombinedApisProperties{}) - android.InitDefaultsModule(module) - return module -} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 000000000000..47d167093b39 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,254 @@ +// 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 api + +import ( + "android/soong/android" + "android/soong/java" + "fmt" + "testing" + + "github.com/google/blueprint/proptools" +) + +var prepareForTestWithCombinedApis = android.GroupFixturePreparers( + android.FixtureRegisterWithContext(registerBuildComponents), + java.PrepareForTestWithJavaBuildComponents, + android.FixtureAddTextFile("a/Android.bp", gatherRequiredDepsForTest()), + java.PrepareForTestWithJavaSdkLibraryFiles, + android.FixtureMergeMockFs(android.MockFS{ + "a/api/current.txt": nil, + "a/api/removed.txt": nil, + "a/api/system-current.txt": nil, + "a/api/system-removed.txt": nil, + "a/api/test-current.txt": nil, + "a/api/test-removed.txt": nil, + "a/api/module-lib-current.txt": nil, + "a/api/module-lib-removed.txt": nil, + }), + android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) { + variables.Allow_missing_dependencies = proptools.BoolPtr(true) + }), +) + +func gatherRequiredDepsForTest() string { + var bp string + + extraLibraryModules := []string{ + "stable.core.platform.api.stubs", + "core-lambda-stubs", + "core.current.stubs", + "ext", + "framework", + "android_stubs_current.from-text", + "android_system_stubs_current.from-text", + "android_test_stubs_current.from-text", + "android_test_frameworks_core_stubs_current.from-text", + "android_module_lib_stubs_current.from-text", + "android_system_server_stubs_current.from-text", + "android_stubs_current.from-source", + "android_system_stubs_current.from-source", + "android_test_stubs_current.from-source", + "android_test_frameworks_core_stubs_current.from-source", + "android_module_lib_stubs_current.from-source", + "android_system_server_stubs_current.from-source", + "android_stubs_current_exportable.from-source", + "android_system_stubs_current_exportable.from-source", + "android_test_stubs_current_exportable.from-source", + "android_module_lib_stubs_current_exportable.from-source", + "android_system_server_stubs_current_exportable.from-source", + "stub-annotations", + } + + extraSdkLibraryModules := []string{ + "framework-virtualization", + "framework-location", + } + + extraSystemModules := []string{ + "core-public-stubs-system-modules", + "core-module-lib-stubs-system-modules", + "stable-core-platform-api-stubs-system-modules", + } + + extraFilegroupModules := []string{ + "non-updatable-current.txt", + "non-updatable-removed.txt", + "non-updatable-system-current.txt", + "non-updatable-system-removed.txt", + "non-updatable-test-current.txt", + "non-updatable-test-removed.txt", + "non-updatable-module-lib-current.txt", + "non-updatable-module-lib-removed.txt", + "non-updatable-system-server-current.txt", + "non-updatable-system-server-removed.txt", + "non-updatable-exportable-current.txt", + "non-updatable-exportable-removed.txt", + "non-updatable-exportable-system-current.txt", + "non-updatable-exportable-system-removed.txt", + "non-updatable-exportable-test-current.txt", + "non-updatable-exportable-test-removed.txt", + "non-updatable-exportable-module-lib-current.txt", + "non-updatable-exportable-module-lib-removed.txt", + "non-updatable-exportable-system-server-current.txt", + "non-updatable-exportable-system-server-removed.txt", + } + + for _, extra := range extraLibraryModules { + bp += fmt.Sprintf(` + java_library { + name: "%s", + srcs: ["a.java"], + sdk_version: "none", + system_modules: "stable-core-platform-api-stubs-system-modules", + compile_dex: true, + } + `, extra) + } + + for _, extra := range extraSdkLibraryModules { + bp += fmt.Sprintf(` + java_sdk_library { + name: "%s", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + compile_dex: true, + annotations_enabled: true, + } + `, extra) + } + + for _, extra := range extraFilegroupModules { + bp += fmt.Sprintf(` + filegroup { + name: "%[1]s", + } + `, extra) + } + + for _, extra := range extraSystemModules { + bp += fmt.Sprintf(` + java_system_modules { + name: "%[1]s", + libs: ["%[1]s-lib"], + } + java_library { + name: "%[1]s-lib", + sdk_version: "none", + system_modules: "none", + } + `, extra) + } + + bp += fmt.Sprintf(` + java_defaults { + name: "android.jar_defaults", + } + `) + + return bp +} + +func TestCombinedApisDefaults(t *testing.T) { + + result := android.GroupFixturePreparers( + prepareForTestWithCombinedApis, + java.FixtureWithLastReleaseApis( + "framework-location", "framework-virtualization", "framework-foo", "framework-bar"), + android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) { + variables.VendorVars = map[string]map[string]string{ + "boolean_var": { + "for_testing": "true", + }, + } + }), + ).RunTestWithBp(t, ` + java_sdk_library { + name: "framework-foo", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + annotations_enabled: true, + } + + java_sdk_library { + name: "framework-bar", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + annotations_enabled: true, + } + + combined_apis { + name: "foo", + bootclasspath: [ + "framework-bar", + ] + select(boolean_var_for_testing(), { + true: [ + "framework-foo", + ], + default: [], + }), + } + `) + + subModuleDependsOnSelectAppendedModule := java.CheckModuleHasDependency(t, + result.TestContext, "foo-current.txt", "", "framework-foo") + android.AssertBoolEquals(t, "Submodule expected to depend on the select-appended module", + true, subModuleDependsOnSelectAppendedModule) +} diff --git a/api/go.work b/api/go.work index edd002e7efba..c09bee578b61 100644 --- a/api/go.work +++ b/api/go.work @@ -1,17 +1,17 @@ -go 1.18 +go 1.22 use ( . - ../../../build/soong ../../../build/blueprint + ../../../build/soong ../../../external/go-cmp ../../../external/golang-protobuf ) replace ( android/soong v0.0.0 => ../../../build/soong - google.golang.org/protobuf v0.0.0 => ../../../external/golang-protobuf github.com/google/blueprint v0.0.0 => ../../../build/blueprint github.com/google/go-cmp v0.0.0 => ../../../external/go-cmp go.starlark.net v0.0.0 => ../../../external/starlark-go + google.golang.org/protobuf v0.0.0 => ../../../external/golang-protobuf ) diff --git a/core/api/current.txt b/core/api/current.txt index 0f237212c768..69c409bb5261 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26569,7 +26569,7 @@ package android.media.midi { package android.media.projection { public final class MediaProjection { - method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); + method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler); method public void stop(); method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback); @@ -54862,6 +54862,8 @@ package android.view.accessibility { method @Deprecated public void addAction(int); method public void addChild(android.view.View); method public void addChild(android.view.View, int); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View, int); method public boolean canOpenPopup(); method public int describeContents(); method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String); @@ -54890,6 +54892,7 @@ package android.view.accessibility { method public int getInputType(); method public android.view.accessibility.AccessibilityNodeInfo getLabelFor(); method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy(); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") @NonNull public java.util.List<android.view.accessibility.AccessibilityNodeInfo> getLabeledByList(); method public int getLiveRegion(); method public int getMaxTextLength(); method @NonNull public java.time.Duration getMinDurationBetweenContentChanges(); @@ -54950,6 +54953,8 @@ package android.view.accessibility { method public boolean removeAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction); method public boolean removeChild(android.view.View); method public boolean removeChild(android.view.View, int); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View, int); method public void setAccessibilityDataSensitive(boolean); method public void setAccessibilityFocused(boolean); method public void setAvailableExtraData(java.util.List<java.lang.String>); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 44c4ab4e1b57..88b5275d37f8 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -423,6 +423,7 @@ package android.app { public final class PictureInPictureParams implements android.os.Parcelable { method public float getAspectRatioFloat(); method public float getExpandedAspectRatioFloat(); + method public static boolean isSameAspectRatio(@NonNull android.graphics.Rect, @NonNull android.util.Rational); } public final class PictureInPictureUiState implements android.os.Parcelable { @@ -618,6 +619,7 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void resetDefaultCrossProfileIntentFilters(int); method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void resetShouldAllowBypassingDevicePolicyManagementRoleQualificationState(); method @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int); + method @FlaggedApi("android.app.admin.flags.provisioning_context_parameter") @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int, @Nullable String); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwner(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, int); method public void setDeviceOwnerType(@NonNull android.content.ComponentName, int); diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 845a346be593..ac3711366ec7 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -1381,6 +1381,18 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim } int toId = findLatestEventIdForTime(playTime); handleAnimationEvents(-1, toId, playTime); + + if (mSeekState.isActive()) { + // Pump a frame to the on-going animators + for (int i = 0; i < mPlayingSet.size(); i++) { + Node node = mPlayingSet.get(i); + if (!node.mEnded) { + pulseFrame(node, getPlayTimeForNodeIncludingDelay(playTime, node)); + } + } + } + + // Remove all the finished anims for (int i = mPlayingSet.size() - 1; i >= 0; i--) { if (mPlayingSet.get(i).mEnded) { mPlayingSet.remove(i); diff --git a/core/java/android/animation/OWNERS b/core/java/android/animation/OWNERS index f3b330a02116..5223c870824c 100644 --- a/core/java/android/animation/OWNERS +++ b/core/java/android/animation/OWNERS @@ -3,3 +3,4 @@ romainguy@google.com tianliu@google.com adamp@google.com +mount@google.com diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 36b1eaba89df..6df971a9cea8 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -2105,8 +2105,7 @@ public final class ActivityThread extends ClientTransactionHandler @Override public void scheduleTaskFragmentTransaction(@NonNull ITaskFragmentOrganizer organizer, @NonNull TaskFragmentTransaction transaction) throws RemoteException { - // TODO(b/260873529): ITaskFragmentOrganizer can be cleanup to be a IBinder token - // after flag removal. + // TODO(b/352665082): ITaskFragmentOrganizer can be cleanup to be a IBinder token organizer.onTransactionReady(transaction); } diff --git a/core/java/android/app/PictureInPictureParams.java b/core/java/android/app/PictureInPictureParams.java index 96d874ee110b..afe915eece26 100644 --- a/core/java/android/app/PictureInPictureParams.java +++ b/core/java/android/app/PictureInPictureParams.java @@ -654,6 +654,33 @@ public final class PictureInPictureParams implements Parcelable { && !hasSetSubtitle() && mIsLaunchIntoPip == null; } + /** + * Compare a given {@link Rect} against the aspect ratio, with rounding error tolerance. + * @param bounds The {@link Rect} represents the source rect hint, this check is not needed + * if app provides a null source rect hint. + * @param aspectRatio {@link Rational} representation of aspect ratio, this check is not needed + * if app provides a null aspect ratio. + * @return {@code true} if the given {@link Rect} matches the aspect ratio. + * @hide + */ + @SuppressWarnings("UnflaggedApi") + @TestApi + public static boolean isSameAspectRatio(@NonNull Rect bounds, @NonNull Rational aspectRatio) { + // Validations + if (bounds.isEmpty() || aspectRatio.floatValue() <= 0) { + return false; + } + // Check against both the width and height. + final int exactWidth = (aspectRatio.getNumerator() * bounds.height()) + / aspectRatio.getDenominator(); + if (Math.abs(exactWidth - bounds.width()) <= 1) { + return true; + } + final int exactHeight = (aspectRatio.getDenominator() * bounds.width()) + / aspectRatio.getNumerator(); + return Math.abs(exactHeight - bounds.height()) <= 1; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 9437c748ee80..e73f4718732f 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -468,6 +468,11 @@ public final class SystemServiceRegistry { public VpnManager createService(ContextImpl ctx) throws ServiceNotFoundException { IBinder b = ServiceManager.getService(Context.VPN_MANAGEMENT_SERVICE); IVpnManager service = IVpnManager.Stub.asInterface(b); + if (service == null + && ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) + && android.server.Flags.allowRemovingVpnService()) { + throw new ServiceNotFoundException(Context.VPN_MANAGEMENT_SERVICE); + } return new VpnManager(ctx, service); }}); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index fb0ce0d2d077..a7070b910f17 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -9202,6 +9202,14 @@ public class DevicePolicyManager { /** * @hide */ + @UnsupportedAppUsage + public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) { + setActiveAdmin(policyReceiver, refreshing, myUserId()); + } + + /** + * @hide + */ @TestApi @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @RequiresPermission(allOf = { @@ -9210,21 +9218,45 @@ public class DevicePolicyManager { }) public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing, int userHandle) { - if (mService != null) { - try { - mService.setActiveAdmin(policyReceiver, refreshing, userHandle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + setActiveAdminInternal(policyReceiver, refreshing, userHandle, null); } /** * @hide */ - @UnsupportedAppUsage - public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) { - setActiveAdmin(policyReceiver, refreshing, myUserId()); + @TestApi + @RequiresPermission(allOf = { + MANAGE_DEVICE_ADMINS, + INTERACT_ACROSS_USERS_FULL + }) + @FlaggedApi(Flags.FLAG_PROVISIONING_CONTEXT_PARAMETER) + public void setActiveAdmin( + @NonNull ComponentName policyReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { + setActiveAdminInternal(policyReceiver, refreshing, userHandle, provisioningContext); + } + + private void setActiveAdminInternal( + @NonNull ComponentName policyReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { + if (mService != null) { + try { + mService.setActiveAdmin( + policyReceiver, + refreshing, + userHandle, + provisioningContext + ); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -9678,7 +9710,7 @@ public class DevicePolicyManager { if (mService != null) { try { final int myUserId = myUserId(); - mService.setActiveAdmin(admin, false, myUserId); + mService.setActiveAdmin(admin, false, myUserId, null); return mService.setProfileOwner(admin, myUserId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index d1837132e1a4..381f9963789b 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -160,7 +160,8 @@ interface IDevicePolicyManager { void setKeyguardDisabledFeatures(in ComponentName who, String callerPackageName, int which, boolean parent); int getKeyguardDisabledFeatures(in ComponentName who, int userHandle, boolean parent); - void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, int userHandle); + void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, + int userHandle, String provisioningContext); boolean isAdminActive(in ComponentName policyReceiver, int userHandle); List<ComponentName> getActiveAdmins(int userHandle); @UnsupportedAppUsage diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 82271129c69e..c789af32e2b1 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -393,3 +393,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "provisioning_context_parameter" + namespace: "enterprise" + description: "Add provisioningContext to store metadata about when the admin was set" + bug: "326525847" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/servertransaction/ObjectPool.java b/core/java/android/app/servertransaction/ObjectPool.java index 598bd8a75637..e86ca37b99ca 100644 --- a/core/java/android/app/servertransaction/ObjectPool.java +++ b/core/java/android/app/servertransaction/ObjectPool.java @@ -16,70 +16,39 @@ package android.app.servertransaction; -import com.android.window.flags.Flags; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - /** * An object pool that can provide reused objects if available. + * * @hide + * @deprecated This class is deprecated. Directly create new instances of objects instead of + * obtaining them from this pool. + * TODO(b/311089192): Clean up usages of the pool. */ +@Deprecated class ObjectPool { - private static final Object sPoolSync = new Object(); - private static final Map<Class, ArrayList<? extends ObjectPoolItem>> sPoolMap = - new HashMap<>(); - - private static final int MAX_POOL_SIZE = 50; - /** * Obtain an instance of a specific class from the pool - * @param itemClass The class of the object we're looking for. + * + * @param ignoredItemClass The class of the object we're looking for. * @return An instance or null if there is none. + * @deprecated This method is deprecated. Directly create new instances of objects instead of + * obtaining them from this pool. */ - public static <T extends ObjectPoolItem> T obtain(Class<T> itemClass) { - if (Flags.disableObjectPool()) { - return null; - } - synchronized (sPoolSync) { - @SuppressWarnings("unchecked") - final ArrayList<T> itemPool = (ArrayList<T>) sPoolMap.get(itemClass); - if (itemPool != null && !itemPool.isEmpty()) { - return itemPool.remove(itemPool.size() - 1); - } - return null; - } + @Deprecated + public static <T extends ObjectPoolItem> T obtain(Class<T> ignoredItemClass) { + return null; } /** * Recycle the object to the pool. The object should be properly cleared before this. - * @param item The object to recycle. + * + * @param ignoredItem The object to recycle. * @see ObjectPoolItem#recycle() + * @deprecated This method is deprecated. The object pool is no longer used, so there's + * no need to recycle objects. */ - public static <T extends ObjectPoolItem> void recycle(T item) { - if (Flags.disableObjectPool()) { - return; - } - synchronized (sPoolSync) { - @SuppressWarnings("unchecked") - ArrayList<T> itemPool = (ArrayList<T>) sPoolMap.get(item.getClass()); - if (itemPool == null) { - itemPool = new ArrayList<>(); - sPoolMap.put(item.getClass(), itemPool); - } - // Check if the item is already in the pool - final int size = itemPool.size(); - for (int i = 0; i < size; i++) { - if (itemPool.get(i) == item) { - throw new IllegalStateException("Trying to recycle already recycled item"); - } - } - - if (size < MAX_POOL_SIZE) { - itemPool.add(item); - } - } + @Deprecated + public static <T extends ObjectPoolItem> void recycle(T ignoredItem) { } } diff --git a/core/java/android/app/servertransaction/ObjectPoolItem.java b/core/java/android/app/servertransaction/ObjectPoolItem.java index 17bd4f30640f..0141f6eff53b 100644 --- a/core/java/android/app/servertransaction/ObjectPoolItem.java +++ b/core/java/android/app/servertransaction/ObjectPoolItem.java @@ -18,12 +18,20 @@ package android.app.servertransaction; /** * Base interface for all lifecycle items that can be put in object pool. + * * @hide + * @deprecated This interface is deprecated. Objects should no longer be pooled. + * TODO(b/311089192): Clean up usages of this interface. */ +@Deprecated public interface ObjectPoolItem { /** * Clear the contents of the item and putting it to a pool. The implementation should call * {@link ObjectPool#recycle(ObjectPoolItem)} passing itself. + * + * @deprecated This method is deprecated. The object pool is no longer used, so there's + * no need to recycle objects. */ + @Deprecated void recycle(); } diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index c2c7b81871df..5a3970295ac2 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -327,3 +327,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "fix_large_display_private_space_settings" + namespace: "profile_experiences" + description: "Fix tablet and foldable specific bugs for private space" + bug: "342563741" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig index 0f944cfb7f67..51024ba64bd9 100644 --- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig +++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig @@ -36,3 +36,10 @@ flag { description: "Enable identifying midi device using USB sysfs" bug: "333778731" } + +flag { + name: "enable_udc_sysfs_usb_state_update" + namespace: "system_sw_usb" + description: "Enable usb state update based on udc sysfs" + bug: "339241080" +} diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java index 67e21957839a..c7f8878f104e 100644 --- a/core/java/android/os/AggregateBatteryConsumer.java +++ b/core/java/android/os/AggregateBatteryConsumer.java @@ -55,7 +55,7 @@ public final class AggregateBatteryConsumer extends BatteryConsumer { @Override public void dump(PrintWriter pw, boolean skipEmptyComponents) { - mPowerComponents.dump(pw, skipEmptyComponents); + mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, skipEmptyComponents); } @Override diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java index 744f6a8d61ac..2447ff93fdbc 100644 --- a/core/java/android/os/BatteryConsumer.java +++ b/core/java/android/os/BatteryConsumer.java @@ -19,14 +19,19 @@ package android.os; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.database.Cursor; import android.database.CursorWindow; +import android.util.IntArray; import android.util.Slog; +import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Interface for objects containing battery attribution data. @@ -192,31 +197,106 @@ public abstract class BatteryConsumer { sProcessStateNames[PROCESS_STATE_CACHED] = "cached"; } - private static final int[] SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = { - POWER_COMPONENT_CPU, - POWER_COMPONENT_MOBILE_RADIO, - POWER_COMPONENT_WIFI, - POWER_COMPONENT_BLUETOOTH, - POWER_COMPONENT_AUDIO, - POWER_COMPONENT_VIDEO, - POWER_COMPONENT_FLASHLIGHT, - POWER_COMPONENT_CAMERA, - POWER_COMPONENT_GNSS, + private static final IntArray SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE; + static { + int[] supportedPowerComponents = { + POWER_COMPONENT_CPU, + POWER_COMPONENT_MOBILE_RADIO, + POWER_COMPONENT_WIFI, + POWER_COMPONENT_BLUETOOTH, + POWER_COMPONENT_AUDIO, + POWER_COMPONENT_VIDEO, + POWER_COMPONENT_FLASHLIGHT, + POWER_COMPONENT_CAMERA, + POWER_COMPONENT_GNSS}; + Arrays.sort(supportedPowerComponents); + SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = IntArray.wrap(supportedPowerComponents); }; static final int COLUMN_INDEX_BATTERY_CONSUMER_TYPE = 0; static final int COLUMN_COUNT = 1; /** + * Identifiers of consumed power aggregations per SCREEN state. + * + * @hide + */ + @IntDef(prefix = {"SCREEN_STATE_"}, value = { + SCREEN_STATE_UNSPECIFIED, + SCREEN_STATE_ANY, + SCREEN_STATE_ON, + SCREEN_STATE_OTHER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenState { + } + + public static final int SCREEN_STATE_UNSPECIFIED = 0; + public static final int SCREEN_STATE_ANY = SCREEN_STATE_UNSPECIFIED; + public static final int SCREEN_STATE_ON = 1; + public static final int SCREEN_STATE_OTHER = 2; // Off, doze etc + + public static final int SCREEN_STATE_COUNT = 3; + + private static final String[] sScreenStateNames = new String[SCREEN_STATE_COUNT]; + + static { + // Assign individually to avoid future mismatch + sScreenStateNames[SCREEN_STATE_UNSPECIFIED] = "unspecified"; + sScreenStateNames[SCREEN_STATE_ON] = "on"; + sScreenStateNames[SCREEN_STATE_OTHER] = "off/doze"; + } + + /** + * Identifiers of consumed power aggregations per POWER state. + * + * @hide + */ + @IntDef(prefix = {"POWER_STATE_"}, value = { + POWER_STATE_UNSPECIFIED, + POWER_STATE_ANY, + POWER_STATE_BATTERY, + POWER_STATE_OTHER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PowerState { + } + + public static final int POWER_STATE_UNSPECIFIED = 0; + public static final int POWER_STATE_ANY = POWER_STATE_UNSPECIFIED; + public static final int POWER_STATE_BATTERY = 1; + public static final int POWER_STATE_OTHER = 2; // Plugged in, or on wireless charger, etc. + + public static final int POWER_STATE_COUNT = 3; + + private static final String[] sPowerStateNames = new String[POWER_STATE_COUNT]; + + static { + // Assign individually to avoid future mismatch + sPowerStateNames[POWER_STATE_UNSPECIFIED] = "unspecified"; + sPowerStateNames[POWER_STATE_BATTERY] = "on battery"; + sPowerStateNames[POWER_STATE_OTHER] = "not on battery"; + } + + /** * Identifies power attribution dimensions that a caller is interested in. */ public static final class Dimensions { public final @PowerComponent int powerComponent; public final @ProcessState int processState; + public final @ScreenState int screenState; + public final @PowerState int powerState; - public Dimensions(int powerComponent, int processState) { + public Dimensions(@PowerComponent int powerComponent, @ProcessState int processState) { + this(powerComponent, processState, SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); + } + + public Dimensions(@PowerComponent int powerComponent, int processState, + @ScreenState int screenState, @PowerState int powerState) { this.powerComponent = powerComponent; this.processState = processState; + this.screenState = screenState; + this.powerState = powerState; } @Override @@ -234,6 +314,20 @@ public abstract class BatteryConsumer { sb.append("processState=").append(sProcessStateNames[processState]); dimensionSpecified = true; } + if (screenState != SCREEN_STATE_ANY) { + if (dimensionSpecified) { + sb.append(", "); + } + sb.append("screenState=").append(screenStateToString(screenState)); + dimensionSpecified = true; + } + if (powerState != POWER_STATE_ANY) { + if (dimensionSpecified) { + sb.append(", "); + } + sb.append("powerState=").append(powerStateToString(powerState)); + dimensionSpecified = true; + } if (!dimensionSpecified) { sb.append("any components and process states"); } @@ -242,7 +336,8 @@ public abstract class BatteryConsumer { } public static final Dimensions UNSPECIFIED_DIMENSIONS = - new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY); + new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY, SCREEN_STATE_ANY, + POWER_STATE_ANY); /** * Identifies power attribution dimensions that are captured by a data element of @@ -258,52 +353,93 @@ public abstract class BatteryConsumer { public static final class Key { public final @PowerComponent int powerComponent; public final @ProcessState int processState; + public final @ScreenState int screenState; + public final @PowerState int powerState; final int mPowerModelColumnIndex; final int mPowerColumnIndex; final int mDurationColumnIndex; - private String mShortString; - private Key(int powerComponent, int processState, int powerModelColumnIndex, + private Key(@PowerComponent int powerComponent, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState, int powerModelColumnIndex, int powerColumnIndex, int durationColumnIndex) { this.powerComponent = powerComponent; this.processState = processState; + this.screenState = screenState; + this.powerState = powerState; mPowerModelColumnIndex = powerModelColumnIndex; mPowerColumnIndex = powerColumnIndex; mDurationColumnIndex = durationColumnIndex; } + /** + * Returns true if this key should be included in an enumeration parameterized with + * the supplied dimensions. + */ + boolean matches(@PowerComponent int powerComponent, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + if (powerComponent != POWER_COMPONENT_ANY && this.powerComponent != powerComponent) { + return false; + } + if (processState != PROCESS_STATE_ANY && this.processState != processState) { + return false; + } + if (screenState != SCREEN_STATE_ANY && this.screenState != screenState) { + return false; + } + if (powerState != POWER_STATE_ANY && this.powerState != powerState) { + return false; + } + return true; + } + @SuppressWarnings("EqualsUnsafeCast") @Override public boolean equals(Object o) { // Skipping null and class check for performance final Key key = (Key) o; return powerComponent == key.powerComponent - && processState == key.processState; + && processState == key.processState + && screenState == key.screenState + && powerState == key.powerState; } @Override public int hashCode() { int result = powerComponent; result = 31 * result + processState; + result = 31 * result + screenState; + result = 31 * result + powerState; return result; } /** * Returns a string suitable for use in dumpsys. */ - public String toShortString() { - if (mShortString == null) { - StringBuilder sb = new StringBuilder(); - sb.append(powerComponentIdToString(powerComponent)); - if (processState != PROCESS_STATE_UNSPECIFIED) { - sb.append(':'); - sb.append(processStateToString(processState)); - } - mShortString = sb.toString(); + public static String toString(@PowerComponent int powerComponent, + @ProcessState int processState, @ScreenState int screenState, + @PowerState int powerState) { + StringBuilder sb = new StringBuilder(); + sb.append(powerComponentIdToString(powerComponent)); + if (processState != PROCESS_STATE_UNSPECIFIED) { + sb.append(':'); + sb.append(processStateToString(processState)); + } + if (screenState != SCREEN_STATE_UNSPECIFIED) { + sb.append(":scr-"); + sb.append(sScreenStateNames[screenState]); + } + if (powerState != POWER_STATE_UNSPECIFIED) { + sb.append(":pwr-"); + sb.append(sPowerStateNames[powerState]); } - return mShortString; + return sb.toString(); + } + + @Override + public String toString() { + return toString(powerComponent, processState, screenState, powerState); } } @@ -335,11 +471,18 @@ public abstract class BatteryConsumer { } /** + * Returns the amount of usage time aggregated over the specified dimensions, in millis. + */ + public long getUsageDurationMillis(@NonNull Dimensions dimensions) { + return mPowerComponents.getUsageDurationMillis(dimensions); + } + + /** * Returns keys for various power values attributed to the specified component * held by this BatteryUsageStats object. */ public Key[] getKeys(@PowerComponent int componentId) { - return mData.getKeys(componentId); + return mData.layout.getKeys(componentId); } /** @@ -347,14 +490,16 @@ public abstract class BatteryConsumer { * for all values of other dimensions such as process state. */ public Key getKey(@PowerComponent int componentId) { - return mData.getKey(componentId, PROCESS_STATE_UNSPECIFIED); + return mData.layout.getKey(componentId, PROCESS_STATE_UNSPECIFIED, SCREEN_STATE_UNSPECIFIED, + POWER_STATE_UNSPECIFIED); } /** * Returns the key for the power attributed to the specified component and process state. */ public Key getKey(@PowerComponent int componentId, @ProcessState int processState) { - return mData.getKey(componentId, processState); + return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED, + POWER_STATE_UNSPECIFIED); } /** @@ -365,8 +510,8 @@ public abstract class BatteryConsumer { * @return Amount of consumed power in mAh. */ public double getConsumedPower(@PowerComponent int componentId) { - return mPowerComponents.getConsumedPower( - mData.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED)); + return mPowerComponents.getConsumedPower(componentId, PROCESS_STATE_UNSPECIFIED, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); } /** @@ -388,7 +533,8 @@ public abstract class BatteryConsumer { */ public @PowerModel int getPowerModel(@BatteryConsumer.PowerComponent int componentId) { return mPowerComponents.getPowerModel( - mData.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED)); + mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED)); } /** @@ -507,6 +653,20 @@ public abstract class BatteryConsumer { } /** + * Returns the human-readable name of the specified power state (on battery or not) + */ + public static String powerStateToString(@PowerState int powerState) { + return sPowerStateNames[powerState]; + } + + /** + * Returns the human-readable name of the specified screen state (on or off/doze) + */ + public static String screenStateToString(@ScreenState int screenState) { + return sScreenStateNames[screenState]; + } + + /** * Prints the stats in a human-readable format. */ public void dump(PrintWriter pw) { @@ -591,42 +751,11 @@ public abstract class BatteryConsumer { return new BatteryConsumerData(cursorWindow, cursorRow, layout); } - public Key[] getKeys(int componentId) { - return layout.keys[componentId]; - } - - Key getKeyOrThrow(int componentId, int processState) { - Key key = getKey(componentId, processState); - if (key == null) { - if (processState == PROCESS_STATE_ANY) { - throw new IllegalArgumentException( - "Unsupported power component ID: " + componentId); - } else { - throw new IllegalArgumentException( - "Unsupported power component ID: " + componentId - + " process state: " + processState); - } - } - return key; - } - - Key getKey(int componentId, int processState) { - if (componentId >= POWER_COMPONENT_COUNT) { - return null; - } - - if (processState == PROCESS_STATE_ANY) { - // The 0-th key for each component corresponds to the roll-up, - // across all dimensions. We might as well skip the iteration over the array. - return layout.keys[componentId][0]; - } else { - for (Key key : layout.keys[componentId]) { - if (key.processState == processState) { - return key; - } - } + boolean hasValue(int columnIndex) { + if (mCursorRow == -1) { + return false; } - return null; + return mCursorWindow.getType(mCursorRow, columnIndex) != Cursor.FIELD_TYPE_NULL; } void putInt(int columnIndex, int value) { @@ -693,113 +822,158 @@ public abstract class BatteryConsumer { public final int customPowerComponentCount; public final boolean powerModelsIncluded; public final boolean processStateDataIncluded; - public final Key[][] keys; + public final boolean screenStateDataIncluded; + public final boolean powerStateDataIncluded; + public final Key[] keys; + public final SparseArray<Key> indexedKeys; public final int totalConsumedPowerColumnIndex; public final int firstCustomConsumedPowerColumn; public final int firstCustomUsageDurationColumn; public final int columnCount; - public final Key[][] processStateKeys; + private Key[][] mPerComponentKeys; private BatteryConsumerDataLayout(int firstColumn, String[] customPowerComponentNames, - boolean powerModelsIncluded, boolean includeProcessStateData) { + boolean powerModelsIncluded, boolean includeProcessStateData, + boolean includeScreenState, boolean includePowerState) { this.customPowerComponentNames = customPowerComponentNames; this.customPowerComponentCount = customPowerComponentNames.length; this.powerModelsIncluded = powerModelsIncluded; this.processStateDataIncluded = includeProcessStateData; + this.screenStateDataIncluded = includeScreenState; + this.powerStateDataIncluded = includePowerState; int columnIndex = firstColumn; totalConsumedPowerColumnIndex = columnIndex++; - keys = new Key[POWER_COMPONENT_COUNT][]; + ArrayList<Key> keyList = new ArrayList<>(); + for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) { + if (!includeScreenState && screenState != SCREEN_STATE_UNSPECIFIED) { + continue; + } + for (int powerState = 0; powerState < POWER_STATE_COUNT; powerState++) { + if (!includePowerState && powerState != POWER_STATE_UNSPECIFIED) { + continue; + } + for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) { + columnIndex = addKeys(keyList, powerModelsIncluded, includeProcessStateData, + componentId, screenState, powerState, columnIndex); + } + } + } + + firstCustomConsumedPowerColumn = columnIndex; + columnIndex += customPowerComponentCount; + + firstCustomUsageDurationColumn = columnIndex; + columnIndex += customPowerComponentCount; + + columnCount = columnIndex; - ArrayList<Key> perComponentKeys = new ArrayList<>(); - for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) { - perComponentKeys.clear(); + keys = keyList.toArray(KEY_ARRAY); + indexedKeys = new SparseArray<>(keys.length); + for (int i = 0; i < keys.length; i++) { + Key key = keys[i]; + int index = keyIndex(key.powerComponent, key.processState, key.screenState, + key.powerState); + indexedKeys.put(index, key); + } + } - // Declare the Key for the power component, ignoring other dimensions. - perComponentKeys.add( - new Key(componentId, PROCESS_STATE_ANY, + private int addKeys(List<Key> keys, boolean powerModelsIncluded, + boolean includeProcessStateData, int componentId, + int screenState, int powerState, int columnIndex) { + keys.add(new Key(componentId, PROCESS_STATE_ANY, screenState, powerState, + powerModelsIncluded + ? columnIndex++ + : POWER_MODEL_NOT_INCLUDED, // power model + columnIndex++, // power + columnIndex++ // usage duration + )); + + // Declare Keys for all process states, if needed + if (includeProcessStateData) { + boolean isSupported = SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE + .binarySearch(componentId) >= 0; + if (isSupported) { + for (int processState = 0; processState < PROCESS_STATE_COUNT; + processState++) { + if (processState == PROCESS_STATE_UNSPECIFIED) { + continue; + } + + keys.add(new Key(componentId, processState, screenState, powerState, powerModelsIncluded ? columnIndex++ - : POWER_MODEL_NOT_INCLUDED, // power model + : POWER_MODEL_NOT_INCLUDED, // power model columnIndex++, // power columnIndex++ // usage duration )); - - // Declare Keys for all process states, if needed - if (includeProcessStateData) { - boolean isSupported = false; - for (int id : SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE) { - if (id == componentId) { - isSupported = true; - break; - } - } - if (isSupported) { - for (int processState = 0; processState < PROCESS_STATE_COUNT; - processState++) { - if (processState == PROCESS_STATE_UNSPECIFIED) { - continue; - } - - perComponentKeys.add( - new Key(componentId, processState, - powerModelsIncluded - ? columnIndex++ - : POWER_MODEL_NOT_INCLUDED, // power model - columnIndex++, // power - columnIndex++ // usage duration - )); - } } } - - keys[componentId] = perComponentKeys.toArray(KEY_ARRAY); } + return columnIndex; + } - if (includeProcessStateData) { - processStateKeys = new Key[BatteryConsumer.PROCESS_STATE_COUNT][]; - ArrayList<Key> perProcStateKeys = new ArrayList<>(); - for (int processState = 0; processState < PROCESS_STATE_COUNT; processState++) { - if (processState == PROCESS_STATE_UNSPECIFIED) { - continue; - } + Key getKey(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + return indexedKeys.get(keyIndex(componentId, processState, screenState, powerState)); + } - perProcStateKeys.clear(); - for (int i = 0; i < keys.length; i++) { - for (int j = 0; j < keys[i].length; j++) { - if (keys[i][j].processState == processState) { - perProcStateKeys.add(keys[i][j]); - } + Key getKeyOrThrow(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + Key key = getKey(componentId, processState, screenState, powerState); + if (key == null) { + throw new IllegalArgumentException( + "Unsupported power component ID: " + Key.toString(componentId, processState, + screenState, powerState)); + } + return key; + } + + public Key[] getKeys(@PowerComponent int componentId) { + synchronized (this) { + if (mPerComponentKeys == null) { + mPerComponentKeys = new Key[BatteryConsumer.POWER_COMPONENT_COUNT][]; + } + Key[] componentKeys = mPerComponentKeys[componentId]; + if (componentKeys == null) { + ArrayList<Key> out = new ArrayList<>(); + for (Key key : keys) { + if (key.powerComponent == componentId) { + out.add(key); } } - processStateKeys[processState] = perProcStateKeys.toArray(KEY_ARRAY); + componentKeys = out.toArray(new Key[out.size()]); + mPerComponentKeys[componentId] = componentKeys; } - } else { - processStateKeys = null; + return componentKeys; } + } - firstCustomConsumedPowerColumn = columnIndex; - columnIndex += customPowerComponentCount; - - firstCustomUsageDurationColumn = columnIndex; - columnIndex += customPowerComponentCount; - - columnCount = columnIndex; + private int keyIndex(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + // [CCCCCCPPPSSBB] + // C - component ID + // P - process state + // S - screen state + // B - power state + return componentId << 7 | processState << 4 | screenState << 2 | powerState; } } static BatteryConsumerDataLayout createBatteryConsumerDataLayout( String[] customPowerComponentNames, boolean includePowerModels, - boolean includeProcessStateData) { + boolean includeProcessStateData, boolean includeScreenStateData, + boolean includePowerStateData) { int columnCount = BatteryConsumer.COLUMN_COUNT; columnCount = Math.max(columnCount, AggregateBatteryConsumer.COLUMN_COUNT); columnCount = Math.max(columnCount, UidBatteryConsumer.COLUMN_COUNT); columnCount = Math.max(columnCount, UserBatteryConsumer.COLUMN_COUNT); return new BatteryConsumerDataLayout(columnCount, customPowerComponentNames, - includePowerModels, includeProcessStateData); + includePowerModels, includeProcessStateData, includeScreenStateData, + includePowerStateData); } protected abstract static class BaseBuilder<T extends BaseBuilder<?>> { @@ -816,12 +990,19 @@ public abstract class BatteryConsumer { @Nullable public Key[] getKeys(@PowerComponent int componentId) { - return mData.getKeys(componentId); + return mData.layout.getKeys(componentId); } @Nullable public Key getKey(@PowerComponent int componentId, @ProcessState int processState) { - return mData.getKey(componentId, processState); + return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED, + POWER_STATE_UNSPECIFIED); + } + + @Nullable + public Key getKey(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + return mData.layout.getKey(componentId, processState, screenState, powerState); } /** diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index 61cc23d994f3..dd484f6bace5 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -102,9 +102,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { static final String XML_ATTR_SCOPE = "scope"; static final String XML_ATTR_PREFIX_CUSTOM_COMPONENT = "custom_component_"; static final String XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA = "includes_proc_state_data"; + static final String XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA = "includes_screen_state_data"; + static final String XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA = "includes_power_state_data"; static final String XML_ATTR_START_TIMESTAMP = "start_timestamp"; static final String XML_ATTR_END_TIMESTAMP = "end_timestamp"; static final String XML_ATTR_PROCESS_STATE = "process_state"; + static final String XML_ATTR_SCREEN_STATE = "screen_state"; + static final String XML_ATTR_POWER_STATE = "power_state"; static final String XML_ATTR_POWER = "power"; static final String XML_ATTR_DURATION = "duration"; static final String XML_ATTR_MODEL = "model"; @@ -144,10 +148,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private final String[] mCustomPowerComponentNames; private final boolean mIncludesPowerModels; private final boolean mIncludesProcessStateData; + private final boolean mIncludesScreenStateData; + private final boolean mIncludesPowerStateData; private final List<UidBatteryConsumer> mUidBatteryConsumers; private final List<UserBatteryConsumer> mUserBatteryConsumers; private final AggregateBatteryConsumer[] mAggregateBatteryConsumers; private final BatteryStatsHistory mBatteryStatsHistory; + private BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout; private CursorWindow mBatteryConsumersCursorWindow; private BatteryUsageStats(@NonNull Builder builder) { @@ -165,6 +172,9 @@ public final class BatteryUsageStats implements Parcelable, Closeable { mCustomPowerComponentNames = builder.mCustomPowerComponentNames; mIncludesPowerModels = builder.mIncludePowerModels; mIncludesProcessStateData = builder.mIncludesProcessStateData; + mIncludesScreenStateData = builder.mIncludesScreenStateData; + mIncludesPowerStateData = builder.mIncludesPowerStateData; + mBatteryConsumerDataLayout = builder.mBatteryConsumerDataLayout; mBatteryConsumersCursorWindow = builder.mBatteryConsumersCursorWindow; double totalPowerMah = 0; @@ -347,11 +357,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { mCustomPowerComponentNames = source.readStringArray(); mIncludesPowerModels = source.readBoolean(); mIncludesProcessStateData = source.readBoolean(); + mIncludesScreenStateData = source.readBoolean(); + mIncludesPowerStateData = source.readBoolean(); mBatteryConsumersCursorWindow = CursorWindow.newFromParcel(source); - BatteryConsumer.BatteryConsumerDataLayout dataLayout = - BatteryConsumer.createBatteryConsumerDataLayout(mCustomPowerComponentNames, - mIncludesPowerModels, mIncludesProcessStateData); + mBatteryConsumerDataLayout = BatteryConsumer.createBatteryConsumerDataLayout( + mCustomPowerComponentNames, mIncludesPowerModels, mIncludesProcessStateData, + mIncludesScreenStateData, mIncludesPowerStateData); final int numRows = mBatteryConsumersCursorWindow.getNumRows(); @@ -363,7 +375,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { for (int i = 0; i < numRows; i++) { final BatteryConsumer.BatteryConsumerData data = new BatteryConsumer.BatteryConsumerData(mBatteryConsumersCursorWindow, i, - dataLayout); + mBatteryConsumerDataLayout); int consumerType = mBatteryConsumersCursorWindow.getInt(i, BatteryConsumer.COLUMN_INDEX_BATTERY_CONSUMER_TYPE); @@ -405,6 +417,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable { dest.writeStringArray(mCustomPowerComponentNames); dest.writeBoolean(mIncludesPowerModels); dest.writeBoolean(mIncludesProcessStateData); + dest.writeBoolean(mIncludesScreenStateData); + dest.writeBoolean(mIncludesPowerStateData); mBatteryConsumersCursorWindow.writeToParcel(dest, flags); @@ -598,23 +612,16 @@ public final class BatteryUsageStats implements Parcelable, Closeable { for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - for (BatteryConsumer.Key key : deviceConsumer.getKeys(componentId)) { - final double devicePowerMah = deviceConsumer.getConsumedPower(key); - final double appsPowerMah = appsConsumer.getConsumedPower(key); - if (devicePowerMah == 0 && appsPowerMah == 0) { - continue; - } - - String label = BatteryConsumer.powerComponentIdToString(componentId); - if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { - label = label - + "(" + BatteryConsumer.processStateToString(key.processState) + ")"; - } - printPowerComponent(pw, prefix, label, devicePowerMah, appsPowerMah, - mIncludesPowerModels ? deviceConsumer.getPowerModel(key) - : BatteryConsumer.POWER_MODEL_UNDEFINED, - deviceConsumer.getUsageDurationMillis(key)); + final double devicePowerMah = deviceConsumer.getConsumedPower(componentId); + final double appsPowerMah = appsConsumer.getConsumedPower(componentId); + if (devicePowerMah == 0 && appsPowerMah == 0) { + continue; } + + printPowerComponent(pw, prefix, BatteryConsumer.powerComponentIdToString(componentId), + devicePowerMah, appsPowerMah, + BatteryConsumer.POWER_MODEL_UNDEFINED, + deviceConsumer.getUsageDurationMillis(componentId)); } for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; @@ -635,6 +642,59 @@ public final class BatteryUsageStats implements Parcelable, Closeable { deviceConsumer.getUsageDurationForCustomComponentMillis(componentId)); } + if (mIncludesScreenStateData || mIncludesPowerStateData) { + String prefixPlus = prefix + " "; + StringBuilder stateLabel = new StringBuilder(); + int screenState = BatteryConsumer.SCREEN_STATE_UNSPECIFIED; + int powerState = BatteryConsumer.POWER_STATE_UNSPECIFIED; + for (BatteryConsumer.Key key : mBatteryConsumerDataLayout.keys) { + if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { + continue; + } + + if (key.screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED + && key.powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) { + // Totals already printed earlier in this method + continue; + } + + final double devicePowerMah = deviceConsumer.getConsumedPower(key); + final double appsPowerMah = appsConsumer.getConsumedPower(key); + if (devicePowerMah == 0 && appsPowerMah == 0) { + continue; + } + + if (key.screenState != screenState || key.powerState != powerState) { + screenState = key.screenState; + powerState = key.powerState; + + boolean empty = true; + stateLabel.setLength(0); + stateLabel.append(" ("); + if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) { + stateLabel.append(BatteryConsumer.powerStateToString(powerState)); + empty = false; + } + if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + if (!empty) { + stateLabel.append(", "); + } + stateLabel.append("screen ").append( + BatteryConsumer.screenStateToString(screenState)); + empty = false; + } + if (!empty) { + stateLabel.append(")"); + pw.println(stateLabel); + } + } + String label = BatteryConsumer.powerComponentIdToString(key.powerComponent); + printPowerComponent(pw, prefixPlus, label, devicePowerMah, appsPowerMah, + mIncludesPowerModels ? deviceConsumer.getPowerModel(key) + : BatteryConsumer.POWER_MODEL_UNDEFINED, + deviceConsumer.getUsageDurationMillis(key)); + } + } dumpSortedBatteryConsumers(pw, prefix, getUidBatteryConsumers()); dumpSortedBatteryConsumers(pw, prefix, getUserBatteryConsumers()); pw.println(); @@ -643,7 +703,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private void printPowerComponent(PrintWriter pw, String prefix, String label, double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) { StringBuilder sb = new StringBuilder(); - sb.append(prefix).append(" ").append(label).append(": ") + sb.append(prefix).append(" ").append(label).append(": ") .append(BatteryStats.formatCharge(devicePowerMah)); if (powerModel != BatteryConsumer.POWER_MODEL_UNDEFINED && powerModel != BatteryConsumer.POWER_MODEL_POWER_PROFILE) { @@ -657,7 +717,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { BatteryStats.formatTimeMs(sb, durationMs); } - pw.println(sb.toString()); + pw.println(sb); } private void dumpSortedBatteryConsumers(PrintWriter pw, String prefix, @@ -670,9 +730,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable { continue; } pw.print(prefix); - pw.print(" "); + pw.print(" "); consumer.dump(pw); - pw.println(); } } @@ -686,6 +745,10 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, mIncludesProcessStateData); + serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA, + mIncludesScreenStateData); + serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA, + mIncludesPowerStateData); serializer.attributeLong(null, XML_ATTR_START_TIMESTAMP, mStatsStartTimestampMs); serializer.attributeLong(null, XML_ATTR_END_TIMESTAMP, mStatsEndTimestampMs); serializer.attributeLong(null, XML_ATTR_DURATION, mStatsDurationMs); @@ -732,9 +795,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { final boolean includesProcStateData = parser.getAttributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, false); + final boolean includesScreenStateData = parser.getAttributeBoolean(null, + XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA, false); + final boolean includesPowerStateData = parser.getAttributeBoolean(null, + XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA, false); builder = new Builder(customComponentNames.toArray(new String[0]), true, - includesProcStateData, 0); + includesProcStateData, includesScreenStateData, includesPowerStateData, 0); builder.setStatsStartTimestamp( parser.getAttributeLong(null, XML_ATTR_START_TIMESTAMP)); @@ -818,6 +885,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private final String[] mCustomPowerComponentNames; private final boolean mIncludePowerModels; private final boolean mIncludesProcessStateData; + private final boolean mIncludesScreenStateData; + private final boolean mIncludesPowerStateData; private final double mMinConsumedPowerThreshold; private final BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout; private long mStatsStartTimestampMs; @@ -839,21 +908,24 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private BatteryStatsHistory mBatteryStatsHistory; public Builder(@NonNull String[] customPowerComponentNames) { - this(customPowerComponentNames, false, false, 0); + this(customPowerComponentNames, false, false, false, false, 0); } public Builder(@NonNull String[] customPowerComponentNames, boolean includePowerModels, - boolean includeProcessStateData, double minConsumedPowerThreshold) { + boolean includeProcessStateData, boolean includeScreenStateData, + boolean includesPowerStateData, double minConsumedPowerThreshold) { mBatteryConsumersCursorWindow = new CursorWindow(null, BATTERY_CONSUMER_CURSOR_WINDOW_SIZE); - mBatteryConsumerDataLayout = - BatteryConsumer.createBatteryConsumerDataLayout(customPowerComponentNames, - includePowerModels, includeProcessStateData); + mBatteryConsumerDataLayout = BatteryConsumer.createBatteryConsumerDataLayout( + customPowerComponentNames, includePowerModels, includeProcessStateData, + includeScreenStateData, includesPowerStateData); mBatteryConsumersCursorWindow.setNumColumns(mBatteryConsumerDataLayout.columnCount); mCustomPowerComponentNames = customPowerComponentNames; mIncludePowerModels = includePowerModels; mIncludesProcessStateData = includeProcessStateData; + mIncludesScreenStateData = includeScreenStateData; + mIncludesPowerStateData = includesPowerStateData; mMinConsumedPowerThreshold = minConsumedPowerThreshold; for (int scope = 0; scope < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; scope++) { final BatteryConsumer.BatteryConsumerData data = @@ -869,6 +941,14 @@ public final class BatteryUsageStats implements Parcelable, Closeable { return mIncludesProcessStateData; } + public boolean isScreenStateDataNeeded() { + return mIncludesScreenStateData; + } + + public boolean isPowerStateDataNeeded() { + return mIncludesPowerStateData; + } + /** * Returns true if this Builder is configured to hold data for the specified * custom power component ID. diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java index 203ef47d857e..d0ed297d2bda 100644 --- a/core/java/android/os/BatteryUsageStatsQuery.java +++ b/core/java/android/os/BatteryUsageStatsQuery.java @@ -73,6 +73,10 @@ public final class BatteryUsageStatsQuery implements Parcelable { public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_VIRTUAL_UIDS = 0x0010; + public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE = 0x0020; + + public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040; + private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000; private final int mFlags; @@ -123,6 +127,14 @@ public final class BatteryUsageStatsQuery implements Parcelable { return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0; } + public boolean isScreenStateDataNeeded() { + return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE) != 0; + } + + public boolean isPowerStateDataNeeded() { + return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE) != 0; + } + /** * Returns the power components that should be estimated or null if all power components * are being requested. @@ -297,6 +309,24 @@ public final class BatteryUsageStatsQuery implements Parcelable { } /** + * Requests that screen state data (screen-on, screen-other) be included in the + * BatteryUsageStats, if available. + */ + public Builder includeScreenStateData() { + mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE; + return this; + } + + /** + * Requests that power state data (on-battery, power-other) be included in the + * BatteryUsageStats, if available. + */ + public Builder includePowerStateData() { + mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE; + return this; + } + + /** * Requests to aggregate stored snapshots between the two supplied timestamps * @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis() * @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis() diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index c9f207cf26e8..50242bad191b 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -29,7 +29,7 @@ import java.io.FileDescriptor; * interface describes the abstract protocol for interacting with a * remotable object. Do not implement this interface directly, instead * extend from {@link Binder}. - * + * * <p>The key IBinder API is {@link #transact transact()} matched by * {@link Binder#onTransact Binder.onTransact()}. These * methods allow you to send a call to an IBinder object and receive a @@ -40,7 +40,7 @@ import java.io.FileDescriptor; * expected behavior when calling an object that exists in the local * process, and the underlying inter-process communication (IPC) mechanism * ensures that these same semantics apply when going across processes. - * + * * <p>The data sent through transact() is a {@link Parcel}, a generic buffer * of data that also maintains some meta-data about its contents. The meta * data is used to manage IBinder object references in the buffer, so that those @@ -51,7 +51,7 @@ import java.io.FileDescriptor; * same IBinder object back. These semantics allow IBinder/Binder objects to * be used as a unique identity (to serve as a token or for other purposes) * that can be managed across processes. - * + * * <p>The system maintains a pool of transaction threads in each process that * it runs in. These threads are used to dispatch all * IPCs coming in from other processes. For example, when an IPC is made from @@ -62,7 +62,7 @@ import java.io.FileDescriptor; * thread in process A returns to allow its execution to continue. In effect, * other processes appear to use as additional threads that you did not create * executing in your own process. - * + * * <p>The Binder system also supports recursion across processes. For example * if process A performs a transaction to process B, and process B while * handling that transaction calls transact() on an IBinder that is implemented @@ -70,7 +70,7 @@ import java.io.FileDescriptor; * transaction to finish will take care of calling Binder.onTransact() on the * object being called by B. This ensures that the recursion semantics when * calling remote binder object are the same as when calling local objects. - * + * * <p>When working with remote objects, you often want to find out when they * are no longer valid. There are three ways this can be determined: * <ul> @@ -83,7 +83,7 @@ import java.io.FileDescriptor; * a {@link DeathRecipient} with the IBinder, which will be called when its * containing process goes away. * </ul> - * + * * @see Binder */ public interface IBinder { @@ -95,17 +95,17 @@ public interface IBinder { * The last transaction code available for user commands. */ int LAST_CALL_TRANSACTION = 0x00ffffff; - + /** * IBinder protocol transaction code: pingBinder(). */ int PING_TRANSACTION = ('_'<<24)|('P'<<16)|('N'<<8)|'G'; - + /** * IBinder protocol transaction code: dump internal state. */ int DUMP_TRANSACTION = ('_'<<24)|('D'<<16)|('M'<<8)|'P'; - + /** * IBinder protocol transaction code: execute a shell command. * @hide @@ -129,7 +129,7 @@ public interface IBinder { * across the platform. To support older code, the default implementation * logs the tweet to the main log as a simple emulation of broadcasting * it publicly over the Internet. - * + * * <p>Also, upon completing the dispatch, the object must make a cup * of tea, return it to the caller, and exclaim "jolly good message * old boy!". @@ -142,7 +142,7 @@ public interface IBinder { * its own like counter, and may display this value to the user to indicate the * quality of the app. This is an optional command that applications do not * need to handle, so the default implementation is to do nothing. - * + * * <p>There is no response returned and nothing about the * system will be functionally affected by it, but it will improve the * app's self-esteem. @@ -185,7 +185,8 @@ public interface IBinder { /** * Limit that should be placed on IPC sizes to keep them safely under the - * transaction buffer limit. + * transaction buffer limit. This is a recommendation, and is not the real + * limit. Transactions should be preferred to be even smaller than this. * @hide */ public static final int MAX_IPC_SIZE = 64 * 1024; @@ -206,7 +207,7 @@ public interface IBinder { /** * Check to see if the object still exists. - * + * * @return Returns false if the * hosting process is gone, otherwise the result (always by default * true) returned by the pingBinder() implementation on the other @@ -221,7 +222,7 @@ public interface IBinder { * true, the process may have died while the call is returning. */ public boolean isBinderAlive(); - + /** * Attempt to retrieve a local implementation of an interface * for this Binder object. If null is returned, you will need @@ -232,7 +233,7 @@ public interface IBinder { /** * Print the object's state into the given stream. - * + * * @param fd The raw file descriptor that the dump is being sent to. * @param args additional arguments to the dump request. */ @@ -280,7 +281,7 @@ public interface IBinder { /** * Perform a generic operation with the object. - * + * * @param code The action to perform. This should * be a number between {@link #FIRST_CALL_TRANSACTION} and * {@link #LAST_CALL_TRANSACTION}. @@ -360,13 +361,13 @@ public interface IBinder { * Remove a previously registered death notification. * The recipient will no longer be called if this object * dies. - * + * * @return {@code true} if the <var>recipient</var> is successfully * unlinked, assuring you that its * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method * will not be called; {@code false} if the target IBinder has already * died, meaning the method has been (or soon will be) called. - * + * * @throws java.util.NoSuchElementException if the given * <var>recipient</var> has not been registered with the IBinder, and * the IBinder is still alive. Note that if the <var>recipient</var> diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index a49ee7d00751..0c34c6fea1d0 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -110,92 +110,6 @@ interface INetworkManagementService void shutdown(); /** - ** TETHERING RELATED - **/ - - /** - * Returns true if IP forwarding is enabled - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Use {@code android.net.INetd#ipfwdEnabled}") - boolean getIpForwardingEnabled(); - - /** - * Enables/Disables IP Forwarding - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Avoid using this directly. Instead, enable tethering with " - + "{@code android.net.TetheringManager#startTethering}. See also " - + "{@code INetd#ipfwdEnableForwarding(String)}.") - void setIpForwardingEnabled(boolean enabled); - - /** - * Start tethering services with the specified dhcp server range - * arg is a set of start end pairs defining the ranges. - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "{@code android.net.TetheringManager#startTethering}") - void startTethering(in String[] dhcpRanges); - - /** - * Stop currently running tethering services - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "{@code android.net.TetheringManager#stopTethering(int)}") - void stopTethering(); - - /** - * Returns true if tethering services are started - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Generally track your own tethering requests. " - + "See also {@code android.net.INetd#tetherIsEnabled()}") - boolean isTetheringStarted(); - - /** - * Tethers the specified interface - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Avoid using this directly. Instead, enable tethering with " - + "{@code android.net.TetheringManager#startTethering}. See also " - + "{@code com.android.net.module.util.NetdUtils#tetherInterface}.") - void tetherInterface(String iface); - - /** - * Untethers the specified interface - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Avoid using this directly. Instead, disable " - + "tethering with {@code android.net.TetheringManager#stopTethering(int)}. " - + "See also {@code NetdUtils#untetherInterface}.") - void untetherInterface(String iface); - - /** - * Returns a list of currently tethered interfaces - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "{@code android.net.TetheringManager#getTetheredIfaces()}") - String[] listTetheredInterfaces(); - - /** - * Enables Network Address Translation between two interfaces. - * The address and netmask of the external interface is used for - * the NAT'ed network. - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Avoid using this directly. Instead, enable tethering with " - + "{@code android.net.TetheringManager#startTethering}.") - void enableNat(String internalInterface, String externalInterface); - - /** - * Disables Network Address Translation between two interfaces. - */ - @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553, - publicAlternatives = "Avoid using this directly. Instead, disable tethering with " - + "{@code android.net.TetheringManager#stopTethering(int)}.") - void disableNat(String internalInterface, String externalInterface); - - /** ** DATA USAGE RELATED **/ diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java index b035f123d5db..f22e1eac9643 100644 --- a/core/java/android/os/PowerComponents.java +++ b/core/java/android/os/PowerComponents.java @@ -17,8 +17,12 @@ package android.os; import static android.os.BatteryConsumer.BatteryConsumerDataLayout.POWER_MODEL_NOT_INCLUDED; import static android.os.BatteryConsumer.POWER_COMPONENT_ANY; +import static android.os.BatteryConsumer.POWER_STATE_ANY; +import static android.os.BatteryConsumer.POWER_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.PROCESS_STATE_ANY; import static android.os.BatteryConsumer.PROCESS_STATE_UNSPECIFIED; +import static android.os.BatteryConsumer.SCREEN_STATE_ANY; +import static android.os.BatteryConsumer.SCREEN_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.convertMahToDeciCoulombs; import android.annotation.NonNull; @@ -56,23 +60,100 @@ class PowerComponents { * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh. */ public double getConsumedPower(@NonNull BatteryConsumer.Dimensions dimensions) { - if (dimensions.powerComponent != POWER_COMPONENT_ANY) { - return mData.getDouble(mData.getKeyOrThrow(dimensions.powerComponent, - dimensions.processState).mPowerColumnIndex); - } else if (dimensions.processState != PROCESS_STATE_ANY) { - if (!mData.layout.processStateDataIncluded) { - throw new IllegalArgumentException( - "No data included in BatteryUsageStats for " + dimensions); + return getConsumedPower(dimensions.powerComponent, dimensions.processState, + dimensions.screenState, dimensions.powerState); + } + + /** + * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh. + */ + public double getConsumedPower(@BatteryConsumer.PowerComponent int powerComponent, + @BatteryConsumer.ProcessState int processState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + if (powerComponent == POWER_COMPONENT_ANY && processState == PROCESS_STATE_ANY + && screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) { + return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex); + } + + if (powerComponent != POWER_COMPONENT_ANY + && ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY) + || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY))) { + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, + processState, screenState, powerState); + if (key != null) { + return mData.getDouble(key.mPowerColumnIndex); } - final BatteryConsumer.Key[] keys = - mData.layout.processStateKeys[dimensions.processState]; - double totalPowerMah = 0; - for (int i = keys.length - 1; i >= 0; i--) { - totalPowerMah += mData.getDouble(keys[i].mPowerColumnIndex); + return 0; + } + + if (mData.layout.processStateDataIncluded || mData.layout.screenStateDataIncluded + || mData.layout.powerStateDataIncluded) { + double total = 0; + for (BatteryConsumer.Key key : mData.layout.keys) { + if (key.processState != PROCESS_STATE_UNSPECIFIED + && key.matches(powerComponent, processState, screenState, powerState)) { + total += mData.getDouble(key.mPowerColumnIndex); + } } - return totalPowerMah; + if (total != 0) { + return total; + } + } + + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); + if (key != null) { + return mData.getDouble(key.mPowerColumnIndex); } else { - return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex); + return 0; + } + } + + /** + * Total usage duration by this consumer, aggregated over the specified dimensions, in ms. + */ + public long getUsageDurationMillis(@NonNull BatteryConsumer.Dimensions dimensions) { + return getUsageDurationMillis(dimensions.powerComponent, dimensions.processState, + dimensions.screenState, dimensions.powerState); + } + + /** + * Total usage duration by this consumer, aggregated over the specified dimensions, in ms. + */ + public long getUsageDurationMillis(@BatteryConsumer.PowerComponent int powerComponent, + @BatteryConsumer.ProcessState int processState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + if ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY) + || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY)) { + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, + processState, screenState, powerState); + if (key != null) { + return mData.getLong(key.mDurationColumnIndex); + } + return 0; + } + + if (mData.layout.screenStateDataIncluded || mData.layout.powerStateDataIncluded) { + long total = 0; + for (BatteryConsumer.Key key : mData.layout.keys) { + if (key.processState != PROCESS_STATE_UNSPECIFIED + && key.matches(powerComponent, processState, screenState, powerState)) { + total += mData.getLong(key.mDurationColumnIndex); + } + } + if (total != 0) { + return total; + } + } + + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); + if (key != null) { + return mData.getLong(key.mDurationColumnIndex); + } else { + return 0; } } @@ -84,7 +165,11 @@ class PowerComponents { * @return Amount of consumed power in mAh. */ public double getConsumedPower(@NonNull BatteryConsumer.Key key) { - return mData.getDouble(key.mPowerColumnIndex); + if (mData.hasValue(key.mPowerColumnIndex)) { + return mData.getDouble(key.mPowerColumnIndex); + } + return getConsumedPower(key.powerComponent, key.processState, key.screenState, + key.powerState); } /** @@ -135,7 +220,12 @@ class PowerComponents { * @return Amount of time in milliseconds. */ public long getUsageDurationMillis(BatteryConsumer.Key key) { - return mData.getLong(key.mDurationColumnIndex); + if (mData.hasValue(key.mDurationColumnIndex)) { + return mData.getLong(key.mDurationColumnIndex); + } + + return getUsageDurationMillis(key.powerComponent, key.processState, key.screenState, + key.powerState); } /** @@ -154,51 +244,77 @@ class PowerComponents { } } - public void dump(PrintWriter pw, boolean skipEmptyComponents) { - String separator = ""; + void dump(PrintWriter pw, @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) { StringBuilder sb = new StringBuilder(); - for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - for (BatteryConsumer.Key key: mData.getKeys(componentId)) { - final double componentPower = getConsumedPower(key); - final long durationMs = getUsageDurationMillis(key); - if (skipEmptyComponents && componentPower == 0 && durationMs == 0) { - continue; + dump(sb, componentId, PROCESS_STATE_ANY, screenState, powerState, skipEmptyComponents); + if (mData.layout.processStateDataIncluded) { + for (int processState = 0; processState < BatteryConsumer.PROCESS_STATE_COUNT; + processState++) { + if (processState == PROCESS_STATE_UNSPECIFIED) { + continue; + } + dump(sb, componentId, processState, screenState, powerState, + skipEmptyComponents); } + } + } - sb.append(separator); - separator = " "; - sb.append(key.toShortString()); - sb.append("="); - sb.append(BatteryStats.formatCharge(componentPower)); - - if (durationMs != 0) { - sb.append(" ("); - BatteryStats.formatTimeMsNoSpace(sb, durationMs); - sb.append(")"); + // TODO(b/352835319): take into account screen and power states + if (screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) { + final int customComponentCount = mData.layout.customPowerComponentCount; + for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + + customComponentCount; + customComponentId++) { + final double customComponentPower = + getConsumedPowerForCustomComponent(customComponentId); + if (skipEmptyComponents && customComponentPower == 0) { + continue; } + sb.append(getCustomPowerComponentName(customComponentId)); + sb.append("="); + sb.append(BatteryStats.formatCharge(customComponentPower)); + sb.append(" "); } } - final int customComponentCount = mData.layout.customPowerComponentCount; - for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; - customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID - + customComponentCount; - customComponentId++) { - final double customComponentPower = - getConsumedPowerForCustomComponent(customComponentId); - if (skipEmptyComponents && customComponentPower == 0) { - continue; - } - sb.append(separator); - separator = " "; - sb.append(getCustomPowerComponentName(customComponentId)); - sb.append("="); - sb.append(BatteryStats.formatCharge(customComponentPower)); + // Remove trailing spaces + while (!sb.isEmpty() && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.setLength(sb.length() - 1); } - pw.print(sb); + pw.println(sb); + } + + private void dump(StringBuilder sb, @BatteryConsumer.PowerComponent int powerComponent, + @BatteryConsumer.ProcessState int processState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) { + final double componentPower = getConsumedPower(powerComponent, processState, screenState, + powerState); + final long durationMs = getUsageDurationMillis(powerComponent, processState, screenState, + powerState); + if (skipEmptyComponents && componentPower == 0 && durationMs == 0) { + return; + } + + sb.append(BatteryConsumer.powerComponentIdToString(powerComponent)); + if (processState != PROCESS_STATE_UNSPECIFIED) { + sb.append(':'); + sb.append(BatteryConsumer.processStateToString(processState)); + } + sb.append("="); + sb.append(BatteryStats.formatCharge(componentPower)); + + if (durationMs != 0) { + sb.append(" ("); + BatteryStats.formatTimeMsNoSpace(sb, durationMs); + sb.append(")"); + } + sb.append(' '); } /** Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto. */ @@ -220,11 +336,13 @@ class PowerComponents { for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - - final BatteryConsumer.Key[] keys = mData.getKeys(componentId); + final BatteryConsumer.Key[] keys = mData.layout.getKeys(componentId); for (BatteryConsumer.Key key : keys) { - final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower(key)); - final long durationMs = getUsageDurationMillis(key); + final long powerDeciCoulombs = convertMahToDeciCoulombs( + getConsumedPower(key.powerComponent, key.processState, key.screenState, + key.powerState)); + final long durationMs = getUsageDurationMillis(key.powerComponent, key.processState, + key.screenState, key.powerState); if (powerDeciCoulombs == 0 && durationMs == 0) { // No interesting data. Make sure not to even write the COMPONENT int. @@ -329,34 +447,43 @@ class PowerComponents { void writeToXml(TypedXmlSerializer serializer) throws IOException { serializer.startTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS); - for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; - componentId++) { - final BatteryConsumer.Key[] keys = mData.getKeys(componentId); - for (BatteryConsumer.Key key : keys) { - final double powerMah = getConsumedPower(key); - final long durationMs = getUsageDurationMillis(key); - if (powerMah == 0 && durationMs == 0) { - continue; - } + for (BatteryConsumer.Key key : mData.layout.keys) { + if (!mData.hasValue(key.mPowerColumnIndex) + && !mData.hasValue(key.mDurationColumnIndex)) { + continue; + } - serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT); - serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId); - if (key.processState != PROCESS_STATE_UNSPECIFIED) { - serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE, - key.processState); - } - if (powerMah != 0) { - serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah); - } - if (durationMs != 0) { - serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs); - } - if (mData.layout.powerModelsIncluded) { - serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL, - getPowerModel(key)); - } - serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT); + final double powerMah = getConsumedPower(key); + final long durationMs = getUsageDurationMillis(key); + if (powerMah == 0 && durationMs == 0) { + continue; + } + + serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT); + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, key.powerComponent); + if (key.processState != PROCESS_STATE_UNSPECIFIED) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE, + key.processState); + } + if (key.screenState != SCREEN_STATE_UNSPECIFIED) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_SCREEN_STATE, + key.screenState); + } + if (key.powerState != POWER_STATE_UNSPECIFIED) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_POWER_STATE, + key.powerState); + } + if (powerMah != 0) { + serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah); + } + if (durationMs != 0) { + serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs); + } + if (mData.layout.powerModelsIncluded) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL, + getPowerModel(key)); } + serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT); } final int customComponentEnd = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID @@ -401,6 +528,8 @@ class PowerComponents { case BatteryUsageStats.XML_TAG_COMPONENT: { int componentId = -1; int processState = PROCESS_STATE_UNSPECIFIED; + int screenState = SCREEN_STATE_UNSPECIFIED; + int powerState = POWER_STATE_UNSPECIFIED; double powerMah = 0; long durationMs = 0; int model = BatteryConsumer.POWER_MODEL_UNDEFINED; @@ -412,6 +541,12 @@ class PowerComponents { case BatteryUsageStats.XML_ATTR_PROCESS_STATE: processState = parser.getAttributeInt(i); break; + case BatteryUsageStats.XML_ATTR_SCREEN_STATE: + screenState = parser.getAttributeInt(i); + break; + case BatteryUsageStats.XML_ATTR_POWER_STATE: + powerState = parser.getAttributeInt(i); + break; case BatteryUsageStats.XML_ATTR_POWER: powerMah = parser.getAttributeDouble(i); break; @@ -423,8 +558,8 @@ class PowerComponents { break; } } - final BatteryConsumer.Key key = - builder.mData.getKey(componentId, processState); + final BatteryConsumer.Key key = builder.mData.layout.getKey(componentId, + processState, screenState, powerState); builder.setConsumedPower(key, powerMah, model); builder.setUsageDurationMillis(key, durationMs); break; @@ -468,11 +603,9 @@ class PowerComponents { Builder(BatteryConsumer.BatteryConsumerData data, double minConsumedPowerThreshold) { mData = data; mMinConsumedPowerThreshold = minConsumedPowerThreshold; - for (BatteryConsumer.Key[] keys : mData.layout.keys) { - for (BatteryConsumer.Key key : keys) { - if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { - mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED); - } + for (BatteryConsumer.Key key : mData.layout.keys) { + if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { + mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED); } } } @@ -572,51 +705,41 @@ class PowerComponents { + ", expected: " + mData.layout.customPowerComponentCount); } - for (int componentId = BatteryConsumer.POWER_COMPONENT_COUNT - 1; componentId >= 0; - componentId--) { - final BatteryConsumer.Key[] keys = mData.layout.keys[componentId]; - for (BatteryConsumer.Key key: keys) { - BatteryConsumer.Key otherKey = null; - for (BatteryConsumer.Key aKey: otherData.layout.keys[componentId]) { - if (aKey.equals(key)) { - otherKey = aKey; - break; - } - } - - if (otherKey == null) { - continue; - } + for (BatteryConsumer.Key key : mData.layout.keys) { + BatteryConsumer.Key otherKey = otherData.layout.getKey(key.powerComponent, + key.processState, key.screenState, key.powerState); + if (otherKey == null) { + continue; + } - mData.putDouble(key.mPowerColumnIndex, - mData.getDouble(key.mPowerColumnIndex) - + otherData.getDouble(otherKey.mPowerColumnIndex)); - mData.putLong(key.mDurationColumnIndex, - mData.getLong(key.mDurationColumnIndex) - + otherData.getLong(otherKey.mDurationColumnIndex)); + mData.putDouble(key.mPowerColumnIndex, + mData.getDouble(key.mPowerColumnIndex) + + otherData.getDouble(otherKey.mPowerColumnIndex)); + mData.putLong(key.mDurationColumnIndex, + mData.getLong(key.mDurationColumnIndex) + + otherData.getLong(otherKey.mDurationColumnIndex)); - if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { - continue; - } + if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { + continue; + } - boolean undefined = false; - if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { + boolean undefined = false; + if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { + undefined = true; + } else { + final int powerModel = mData.getInt(key.mPowerModelColumnIndex); + int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex); + if (powerModel == POWER_MODEL_UNINITIALIZED) { + mData.putInt(key.mPowerModelColumnIndex, otherPowerModel); + } else if (powerModel != otherPowerModel + && otherPowerModel != POWER_MODEL_UNINITIALIZED) { undefined = true; - } else { - final int powerModel = mData.getInt(key.mPowerModelColumnIndex); - int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex); - if (powerModel == POWER_MODEL_UNINITIALIZED) { - mData.putInt(key.mPowerModelColumnIndex, otherPowerModel); - } else if (powerModel != otherPowerModel - && otherPowerModel != POWER_MODEL_UNINITIALIZED) { - undefined = true; - } } + } - if (undefined) { - mData.putInt(key.mPowerModelColumnIndex, - BatteryConsumer.POWER_MODEL_UNDEFINED); - } + if (undefined) { + mData.putInt(key.mPowerModelColumnIndex, + BatteryConsumer.POWER_MODEL_UNDEFINED); } } @@ -631,10 +754,8 @@ class PowerComponents { final int usageColumnIndex = mData.layout.firstCustomUsageDurationColumn + i; final int otherDurationColumnIndex = otherData.layout.firstCustomUsageDurationColumn + i; - mData.putLong(usageColumnIndex, - mData.getLong(usageColumnIndex) + otherData.getLong( - otherDurationColumnIndex) - ); + mData.putLong(usageColumnIndex, mData.getLong(usageColumnIndex) + + otherData.getLong(otherDurationColumnIndex)); } } @@ -647,7 +768,8 @@ class PowerComponents { for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { totalPowerMah += mData.getDouble( - mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY).mPowerColumnIndex); + mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_ANY, SCREEN_STATE_ANY, + POWER_STATE_ANY).mPowerColumnIndex); } for (int i = 0; i < mData.layout.customPowerComponentCount; i++) { totalPowerMah += mData.getDouble( @@ -661,19 +783,17 @@ class PowerComponents { */ @NonNull public PowerComponents build() { - for (BatteryConsumer.Key[] keys : mData.layout.keys) { - for (BatteryConsumer.Key key : keys) { - if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { - if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) { - mData.putInt(key.mPowerModelColumnIndex, - BatteryConsumer.POWER_MODEL_UNDEFINED); - } + for (BatteryConsumer.Key key: mData.layout.keys) { + if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { + if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) { + mData.putInt(key.mPowerModelColumnIndex, + BatteryConsumer.POWER_MODEL_UNDEFINED); } + } - if (mMinConsumedPowerThreshold != 0) { - if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) { - mData.putDouble(key.mPowerColumnIndex, 0); - } + if (mMinConsumedPowerThreshold != 0) { + if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) { + mData.putDouble(key.mPowerColumnIndex, 0); } } } diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java index 0be2d3e30c33..e95c6a44c281 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -277,7 +277,7 @@ public final class ServiceManager { if (service != null) { return service; } else { - return Binder.allowBlocking(getIServiceManager().checkService(name)); + return Binder.allowBlocking(getIServiceManager().checkService(name).getBinder()); } } catch (RemoteException e) { Log.e(TAG, "error in checkService", e); @@ -425,7 +425,7 @@ public final class ServiceManager { private static IBinder rawGetService(String name) throws RemoteException { final long start = sStatLogger.getTime(); - final IBinder binder = getIServiceManager().getService(name); + final IBinder binder = getIServiceManager().getService(name).getBinder(); final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start); diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java index 7b91dd5822e7..6c9a5c7f9fff 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -58,12 +58,12 @@ class ServiceManagerProxy implements IServiceManager { } @UnsupportedAppUsage - public IBinder getService(String name) throws RemoteException { + public Service getService(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return mServiceManager.checkService(name); + return checkService(name); } - public IBinder checkService(String name) throws RemoteException { + public Service checkService(String name) throws RemoteException { return mServiceManager.checkService(name); } diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index b5029a6aaff3..2fde5e7eff6a 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -119,7 +119,7 @@ "PowerComponents\\.java", "[^/]*BatteryConsumer[^/]*\\.java" ], - "name": "BatteryUsageStatsProtoTests" + "name": "PowerStatsTests" }, { "file_patterns": ["SharedMemory[^/]*\\.java"], diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java index 53af838cb535..9b5a378d6591 100644 --- a/core/java/android/os/UidBatteryConsumer.java +++ b/core/java/android/os/UidBatteryConsumer.java @@ -140,12 +140,50 @@ public final class UidBatteryConsumer extends BatteryConsumer { skipEmptyComponents); appendProcessStateData(sb, BatteryConsumer.PROCESS_STATE_CACHED, skipEmptyComponents); - pw.print(sb); + pw.println(sb); + } else { + pw.println(); } - pw.print(" ( "); - mPowerComponents.dump(pw, skipEmptyComponents /* skipTotalPowerComponent */); - pw.print(" ) "); + pw.print(" "); + mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, skipEmptyComponents); + + if (mData.layout.powerStateDataIncluded || mData.layout.screenStateDataIncluded) { + for (int powerState = 0; powerState < POWER_STATE_COUNT; powerState++) { + if (mData.layout.powerStateDataIncluded && powerState == POWER_STATE_UNSPECIFIED) { + continue; + } + + for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) { + if (mData.layout.screenStateDataIncluded + && screenState == POWER_STATE_UNSPECIFIED) { + continue; + } + + final double consumedPower = mPowerComponents.getConsumedPower( + POWER_COMPONENT_ANY, + PROCESS_STATE_ANY, screenState, powerState); + if (consumedPower == 0) { + continue; + } + + pw.print(" ("); + if (powerState != POWER_STATE_UNSPECIFIED) { + pw.print(BatteryConsumer.powerStateToString(powerState)); + } + if (screenState != SCREEN_STATE_UNSPECIFIED) { + if (powerState != POWER_STATE_UNSPECIFIED) { + pw.print(", "); + } + pw.print("screen "); + pw.print(BatteryConsumer.screenStateToString(screenState)); + } + pw.print(") "); + mPowerComponents.dump(pw, screenState, powerState, + skipEmptyComponents /* skipTotalPowerComponent */); + } + } + } } private void appendProcessStateData(StringBuilder sb, @ProcessState int processState, @@ -160,10 +198,6 @@ public final class UidBatteryConsumer extends BatteryConsumer { .append(BatteryStats.formatCharge(power)); } - static UidBatteryConsumer create(BatteryConsumerData data) { - return new UidBatteryConsumer(data); - } - /** Serializes this object to XML */ void writeToXml(TypedXmlSerializer serializer) throws IOException { if (getConsumedPower() == 0) { diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java index 23ba0c635eca..ea2be7b80e94 100644 --- a/core/java/android/os/UserBatteryConsumer.java +++ b/core/java/android/os/UserBatteryConsumer.java @@ -60,10 +60,10 @@ public class UserBatteryConsumer extends BatteryConsumer { pw.print("User "); pw.print(getUserId()); pw.print(": "); - pw.print(BatteryStats.formatCharge(consumedPower)); - pw.print(" ( "); - mPowerComponents.dump(pw, skipEmptyComponents /* skipTotalPowerComponent */); - pw.print(" ) "); + pw.println(BatteryStats.formatCharge(consumedPower)); + pw.print(" "); + mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, + skipEmptyComponents /* skipTotalPowerComponent */); } /** Serializes this object to XML */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 2562c8e31095..ff389208a579 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11075,6 +11075,13 @@ public final class Settings { public static final String MANDATORY_BIOMETRICS = "mandatory_biometrics"; /** + * Whether or not requirements for mandatory biometrics is satisfied. + * @hide + */ + public static final String MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED = + "mandatory_biometrics_requirements_satisfied"; + + /** * Whether or not active unlock triggers on wake. * @hide */ diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index e4fc1cd9dad0..fbeab84fa96d 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -787,7 +787,6 @@ public class DreamService extends Service implements Window.Callback { */ public void setInteractive(boolean interactive) { mInteractive = interactive; - updateAccessibilityMessage(); } /** @@ -1641,9 +1640,9 @@ public class DreamService extends Service implements Window.Callback { if (mWindow == null) return; if (mDreamAccessibility == null) { final View rootView = mWindow.getDecorView(); - mDreamAccessibility = new DreamAccessibility(this, rootView); + mDreamAccessibility = new DreamAccessibility(this, rootView, this::wakeUp); } - mDreamAccessibility.updateAccessibilityConfiguration(isInteractive()); + mDreamAccessibility.updateAccessibilityConfiguration(); } private boolean getWindowFlagValue(int flag, boolean defaultValue) { diff --git a/core/java/android/service/dreams/utils/DreamAccessibility.java b/core/java/android/service/dreams/utils/DreamAccessibility.java index c38f41bab5a6..f504ff7d4650 100644 --- a/core/java/android/service/dreams/utils/DreamAccessibility.java +++ b/core/java/android/service/dreams/utils/DreamAccessibility.java @@ -18,6 +18,7 @@ package android.service.dreams.utils; import android.annotation.NonNull; import android.content.Context; +import android.os.Bundle; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; @@ -32,22 +33,22 @@ public class DreamAccessibility { private final Context mContext; private final View mView; private final View.AccessibilityDelegate mAccessibilityDelegate; + private final Runnable mDismissCallback; - public DreamAccessibility(@NonNull Context context, @NonNull View view) { + public DreamAccessibility(@NonNull Context context, @NonNull View view, + @NonNull Runnable dismissCallback) { mContext = context; mView = view; mAccessibilityDelegate = createNewAccessibilityDelegate(mContext); + mDismissCallback = dismissCallback; } /** - * @param interactive - * Removes and add accessibility configuration depending if the dream is interactive or not + * Adds default accessibility configuration if none exist on the dream */ - public void updateAccessibilityConfiguration(Boolean interactive) { - if (!interactive) { + public void updateAccessibilityConfiguration() { + if (mView.getAccessibilityDelegate() == null) { addAccessibilityConfiguration(); - } else { - removeCustomAccessibilityAction(); } } @@ -58,31 +59,28 @@ public class DreamAccessibility { mView.setAccessibilityDelegate(mAccessibilityDelegate); } - /** - * Removes Configured the accessibility actions for the given root view. - */ - private void removeCustomAccessibilityAction() { - if (mView.getAccessibilityDelegate() == mAccessibilityDelegate) { - mView.setAccessibilityDelegate(null); - } - } - private View.AccessibilityDelegate createNewAccessibilityDelegate(Context context) { return new View.AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); - for (AccessibilityNodeInfo.AccessibilityAction action : info.getActionList()) { - if (action.getId() == AccessibilityNodeInfo.ACTION_CLICK) { - info.removeAction(action); - break; - } - } info.addAction(new AccessibilityNodeInfo.AccessibilityAction( - AccessibilityNodeInfo.ACTION_CLICK, + AccessibilityNodeInfo.ACTION_DISMISS, context.getResources().getString(R.string.dream_accessibility_action_click) )); } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + switch(action){ + case AccessibilityNodeInfo.ACTION_DISMISS: + if (mDismissCallback != null) { + mDismissCallback.run(); + } + break; + } + return true; + } }; } } diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java index 5d84d17bdb6e..b07534f5fe52 100644 --- a/core/java/android/text/ClientFlags.java +++ b/core/java/android/text/ClientFlags.java @@ -68,4 +68,11 @@ public class ClientFlags { public static boolean fixMisalignedContextMenu() { return TextFlags.isFeatureEnabled(Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU); } + + /** + * @see Flags#clearFontVariationSettings() + */ + public static boolean clearFontVariationSettings() { + return TextFlags.isFeatureEnabled(Flags.FLAG_CLEAR_FONT_VARIATION_SETTINGS); + } } diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java index 9e02460d2637..4dca284b8a4d 100644 --- a/core/java/android/text/TextFlags.java +++ b/core/java/android/text/TextFlags.java @@ -61,6 +61,7 @@ public final class TextFlags { Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE, Flags.FLAG_ICU_BIDI_MIGRATION, Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU, + Flags.FLAG_CLEAR_FONT_VARIATION_SETTINGS, }; /** @@ -75,6 +76,7 @@ public final class TextFlags { Flags.fixLineHeightForLocale(), Flags.icuBidiMigration(), Flags.fixMisalignedContextMenu(), + Flags.clearFontVariationSettings(), }; /** diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 8836c8a3a113..02c63db2c8a6 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -220,3 +220,23 @@ flag { is_fixed_read_only: true bug: "346915432" } + +flag { + name: "clear_font_variation_settings" + namespace: "text" + description: "The font variation settings must be cleared when the new Typeface is set" + bug: "353609778" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "portuguese_hyphenator" + namespace: "text" + description: "Portuguese taiored hyphenator" + bug: "344656282" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/text/format/DateIntervalFormat.java b/core/java/android/text/format/DateIntervalFormat.java index e8236fda42b6..8dea3228eb0c 100644 --- a/core/java/android/text/format/DateIntervalFormat.java +++ b/core/java/android/text/format/DateIntervalFormat.java @@ -26,6 +26,7 @@ import android.icu.util.ULocale; import android.util.LruCache; import com.android.internal.annotations.VisibleForTesting; +import com.android.libcore.Flags; import java.text.FieldPosition; import java.util.TimeZone; @@ -123,4 +124,14 @@ public final class DateIntervalFormat { && c.get(Calendar.SECOND) == 0 && c.get(Calendar.MILLISECOND) == 0; } + + + @VisibleForTesting(visibility = PACKAGE) + public static boolean isLibcoreVFlagEnabled() { + // Note that the Flags class is expected to be jarjar-ed in the build-time. + // See go/repackage_flags + // The full-qualified name should be like + // com.android.internal.hidden_from_bootclasspath.com.android.libcore.Flags + return Flags.vApis(); + } } diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index 1d950dc44e46..6343313b2e01 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -119,9 +119,11 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override public boolean applyLocalVisibilityOverride() { - ImeTracing.getInstance().triggerClientDump( - "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", - mController.getHost().getInputMethodManager(), null /* icProto */); + if (!Flags.refactorInsetsController()) { + ImeTracing.getInstance().triggerClientDump( + "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", + mController.getHost().getInputMethodManager(), null /* icProto */); + } return super.applyLocalVisibilityOverride(); } @@ -205,9 +207,13 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override public void removeSurface() { - final IBinder window = mController.getHost().getWindowToken(); - if (window != null) { - getImm().removeImeSurface(window); + if (Flags.refactorInsetsController()) { + super.removeSurface(); + } else { + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + getImm().removeImeSurface(window); + } } } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index df2af731037e..f166b89a1d13 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -765,7 +765,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation public InsetsController(Host host) { this(host, (controller, id, type) -> { - if (type == ime()) { + if (!Flags.refactorInsetsController() && type == ime()) { return new ImeInsetsSourceConsumer(id, controller.mState, Transaction::new, controller); } else { diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index c73cbc6e9a57..477e35b6e655 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -43,6 +43,7 @@ import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.inputmethod.ImeTracing; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -296,6 +297,13 @@ public class InsetsSourceConsumer { @VisibleForTesting(visibility = PACKAGE) public boolean applyLocalVisibilityOverride() { + if (Flags.refactorInsetsController()) { + if (mType == WindowInsets.Type.ime()) { + ImeTracing.getInstance().triggerClientDump( + "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", + mController.getHost().getInputMethodManager(), null /* icProto */); + } + } final InsetsSource source = mState.peekSource(mId); if (source == null) { return false; @@ -396,6 +404,14 @@ public class InsetsSourceConsumer { */ public void removeSurface() { // no-op for types that always return ShowResult#SHOW_IMMEDIATELY. + if (Flags.refactorInsetsController()) { + if (mType == WindowInsets.Type.ime()) { + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + mController.getHost().getInputMethodManager().removeImeSurface(window); + } + } + } } @VisibleForTesting(visibility = PACKAGE) diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index fedbe4a65e07..42d66ce6bf1b 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -366,13 +366,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall Log.e(TAG, "Received invalid input event"); return; } - try { - vri.processingBackKey(true); - vri.enqueueInputEvent(keyEvent, null /* receiver */, 0 /* flags */, - true /* processImmediately */); - } finally { - vri.processingBackKey(false); - } + vri.enqueueInputEvent(keyEvent, null /* receiver */, 0 /* flags */, + true /* processImmediately */); }); } }; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 596726f83c15..2f204f9b1be9 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -727,8 +727,6 @@ public final class ViewRootImpl implements ViewParent, boolean mUpcomingWindowFocus; @GuardedBy("this") boolean mUpcomingInTouchMode; - // While set, allow this VRI to handle back key without drop it. - private boolean mProcessingBackKey; /** * Compatibility {@link OnBackInvokedCallback} for windowless window, to forward the back * key event host app. @@ -1135,6 +1133,8 @@ public final class ViewRootImpl implements ViewParent, // Take 24 and 30 as an example, 24 is not a divisor of 30. // We consider there is a conflict. private boolean mIsFrameRateConflicted = false; + // Used to check whether SurfaceControl has been replaced. + private boolean mSurfaceReplaced = false; // Used to set frame rate compatibility. @Surface.FrameRateCompatibility int mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; @@ -3831,6 +3831,7 @@ public final class ViewRootImpl implements ViewParent, surfaceReplaced = (surfaceGenerationId != mSurface.getGenerationId() || surfaceControlChanged) && mSurface.isValid(); if (surfaceReplaced) { + mSurfaceReplaced = true; mSurfaceSequenceId++; } if (alwaysConsumeSystemBarsChanged) { @@ -4443,6 +4444,7 @@ public final class ViewRootImpl implements ViewParent, mPreferredFrameRate = -1; mIsFrameRateConflicted = false; mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN; + mSurfaceReplaced = false; } else if (mPreferredFrameRate == 0) { // From MSG_FRAME_RATE_SETTING, where mPreferredFrameRate is set to 0 setPreferredFrameRate(0); @@ -7265,7 +7267,7 @@ public final class ViewRootImpl implements ViewParent, // Find a reason for dropping or canceling the event. final String reason; // The embedded window is focused, allow this VRI to handle back key. - if (!mAttachInfo.mHasWindowFocus && !(mProcessingBackKey && isBack(q.mEvent)) + if (!mAttachInfo.mHasWindowFocus && !isBack(q.mEvent) && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER) && !isAutofillUiShowing()) { // This is a non-pointer event and the window doesn't currently have input focus @@ -7543,7 +7545,6 @@ public final class ViewRootImpl implements ViewParent, animationCallback.onBackCancelled(); } else { topCallback.onBackInvoked(); - return FINISH_HANDLED; } break; } @@ -7551,14 +7552,16 @@ public final class ViewRootImpl implements ViewParent, if (keyEvent.getAction() == KeyEvent.ACTION_UP) { if (!keyEvent.isCanceled()) { topCallback.onBackInvoked(); - return FINISH_HANDLED; } else { Log.d(mTag, "Skip onBackInvoked(), reason: keyEvent.isCanceled=true"); } } } - - return FINISH_NOT_HANDLED; + if (keyEvent.getAction() == KeyEvent.ACTION_UP) { + // forward a cancelled event so that following stages cancel their back logic + keyEvent.cancel(); + } + return FORWARD; } @Override @@ -11213,11 +11216,6 @@ public final class ViewRootImpl implements ViewParent, mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, listener).sendToTarget(); } - // Make this VRI able to process back key without drop it. - void processingBackKey(boolean processing) { - mProcessingBackKey = processing; - } - /** * Collect and include any ScrollCaptureCallback instances registered with the window. * @@ -12549,15 +12547,8 @@ public final class ViewRootImpl implements ViewParent, * @return whether the event was handled (i.e. onKeyPreIme consumed it if preImeOnly=true) */ public boolean injectBackKeyEvents(boolean preImeOnly) { - boolean consumed; - try { - processingBackKey(true); - sendBackKeyEvent(KeyEvent.ACTION_DOWN, preImeOnly); - consumed = sendBackKeyEvent(KeyEvent.ACTION_UP, preImeOnly); - } finally { - processingBackKey(false); - } - return consumed; + sendBackKeyEvent(KeyEvent.ACTION_DOWN, preImeOnly); + return sendBackKeyEvent(KeyEvent.ACTION_UP, preImeOnly); } private boolean sendBackKeyEvent(int action, boolean preImeOnly) { @@ -12933,8 +12924,9 @@ public final class ViewRootImpl implements ViewParent, boolean traceFrameRateCategory = false; try { - if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT - && mLastPreferredFrameRateCategory != frameRateCategory) { + if ((frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT + && mLastPreferredFrameRateCategory != frameRateCategory) + || mSurfaceReplaced) { traceFrameRateCategory = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); if (traceFrameRateCategory) { String reason = reasonToString(frameRateReason); @@ -12998,7 +12990,7 @@ public final class ViewRootImpl implements ViewParent, boolean traceFrameRate = false; try { - if (mLastPreferredFrameRate != preferredFrameRate) { + if (mLastPreferredFrameRate != preferredFrameRate || mSurfaceReplaced) { traceFrameRate = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); if (traceFrameRate) { Trace.traceBegin( diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index 561d979f2f9d..987c8c8213f3 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -1708,6 +1708,7 @@ public final class WindowInsets { } mTypeBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]); } + mSystemInsetsConsumed = false; return this; } @@ -1736,6 +1737,7 @@ public final class WindowInsets { } mTypeMaxBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]); } + mStableInsetsConsumed = false; return this; } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index a5ba294d6a19..90cfcb1e64a8 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -982,6 +982,7 @@ public class AccessibilityNodeInfo implements Parcelable { private long mParentNodeId = UNDEFINED_NODE_ID; private long mLabelForId = UNDEFINED_NODE_ID; private long mLabeledById = UNDEFINED_NODE_ID; + private LongArray mLabeledByIds; private long mTraversalBefore = UNDEFINED_NODE_ID; private long mTraversalAfter = UNDEFINED_NODE_ID; @@ -3599,6 +3600,131 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Adds the view which serves as the label of the view represented by + * this info for accessibility purposes. When more than one labels are + * added, the content from each label is combined in the order that + * they are added. + * <p> + * If visible text can be used to describe or give meaning to this UI, + * this method is preferred. For example, a TextView before an EditText + * in the UI usually specifies what information is contained in the + * EditText. Hence, the EditText is labelled by the TextView. + * </p> + * + * @param label A view that labels this node's source. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public void addLabeledBy(@NonNull View label) { + addLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID); + } + + /** + * Adds the view which serves as the label of the view represented by + * this info for accessibility purposes. If <code>virtualDescendantId</code> + * is {@link View#NO_ID} the root is set as the label. When more than one + * labels are added, the content from each label is combined in the order + * that they are added. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * If visible text can be used to describe or give meaning to this UI, + * this method is preferred. For example, a TextView before an EditText + * in the UI usually specifies what information is contained in the + * EditText. Hence, the EditText is labelled by the TextView. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root A root whose virtual descendant labels this node's source. + * @param virtualDescendantId The id of the virtual descendant. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public void addLabeledBy(@NonNull View root, int virtualDescendantId) { + enforceNotSealed(); + Preconditions.checkNotNull(root, "%s must not be null", root); + if (mLabeledByIds == null) { + mLabeledByIds = new LongArray(); + } + mLabeledById = makeNodeId(root.getAccessibilityViewId(), virtualDescendantId); + mLabeledByIds.add(mLabeledById); + } + + /** + * Gets the list of node infos which serve as the labels of the view represented by + * this info for accessibility purposes. + * + * @return The list of labels in the order that they were added. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public @NonNull List<AccessibilityNodeInfo> getLabeledByList() { + enforceSealed(); + List<AccessibilityNodeInfo> labels = new ArrayList<>(); + if (mLabeledByIds == null) { + return labels; + } + for (int i = 0; i < mLabeledByIds.size(); i++) { + labels.add(getNodeForAccessibilityId(mConnectionId, mWindowId, mLabeledByIds.get(i))); + } + return labels; + } + + /** + * Removes a label. If the label was not previously added to the node, + * calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param label The node which serves as this node's label. + * @return true if the label was present + * @see #addLabeledBy(View) + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public boolean removeLabeledBy(@NonNull View label) { + return removeLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID); + } + + /** + * Removes a virtual label which is a descendant of the given + * <code>root</code>. If the label was not previously added to the node, + * calling this method has no effect. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual node which serves as this node's label. + * @return true if the label was present + * @see #addLabeledBy(View, int) + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public boolean removeLabeledBy(@NonNull View root, int virtualDescendantId) { + enforceNotSealed(); + final LongArray labeledByIds = mLabeledByIds; + if (labeledByIds == null) { + return false; + } + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + final long labeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + if (mLabeledById == labeledById) { + mLabeledById = UNDEFINED_NODE_ID; + } + final int index = labeledByIds.indexOf(labeledById); + if (index < 0) { + return false; + } + labeledByIds.remove(index); + return true; + } + + /** * Sets the view which serves as the label of the view represented by * this info for accessibility purposes. * @@ -3631,7 +3757,17 @@ public class AccessibilityNodeInfo implements Parcelable { enforceNotSealed(); final int rootAccessibilityViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + if (Flags.supportMultipleLabeledby()) { + if (mLabeledByIds == null) { + mLabeledByIds = new LongArray(); + } else { + mLabeledByIds.clear(); + } + } mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + if (Flags.supportMultipleLabeledby()) { + mLabeledByIds.add(mLabeledById); + } } /** @@ -4242,6 +4378,12 @@ public class AccessibilityNodeInfo implements Parcelable { fieldIndex++; if (mLabeledById != DEFAULT.mLabeledById) nonDefaultFields |= bitAt(fieldIndex); fieldIndex++; + if (Flags.supportMultipleLabeledby()) { + if (!LongArray.elementsEqual(mLabeledByIds, DEFAULT.mLabeledByIds)) { + nonDefaultFields |= bitAt(fieldIndex); + } + fieldIndex++; + } if (mTraversalBefore != DEFAULT.mTraversalBefore) nonDefaultFields |= bitAt(fieldIndex); fieldIndex++; if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex); @@ -4383,6 +4525,20 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mParentNodeId); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabelForId); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById); + if (Flags.supportMultipleLabeledby()) { + if (isBitSet(nonDefaultFields, fieldIndex++)) { + final LongArray labeledByIds = mLabeledByIds; + if (labeledByIds == null) { + parcel.writeInt(0); + } else { + final int labeledByIdsSize = labeledByIds.size(); + parcel.writeInt(labeledByIdsSize); + for (int i = 0; i < labeledByIdsSize; i++) { + parcel.writeLong(labeledByIds.get(i)); + } + } + } + } if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter); if (isBitSet(nonDefaultFields, fieldIndex++)) { @@ -4550,6 +4706,9 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = other.mParentNodeId; mLabelForId = other.mLabelForId; mLabeledById = other.mLabeledById; + if (Flags.supportMultipleLabeledby()) { + mLabeledByIds = other.mLabeledByIds; + } mTraversalBefore = other.mTraversalBefore; mTraversalAfter = other.mTraversalAfter; mMinDurationBetweenContentChanges = other.mMinDurationBetweenContentChanges; @@ -4656,6 +4815,20 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) mParentNodeId = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mLabelForId = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong(); + if (Flags.supportMultipleLabeledby()) { + if (isBitSet(nonDefaultFields, fieldIndex++)) { + final int labeledByIdsSize = parcel.readInt(); + if (labeledByIdsSize <= 0) { + mLabeledByIds = null; + } else { + mLabeledByIds = new LongArray(labeledByIdsSize); + for (int i = 0; i < labeledByIdsSize; i++) { + final long labeledById = parcel.readLong(); + mLabeledByIds.add(labeledById); + } + } + } + } if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 44c1acc34273..ed2bf79c51f4 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -184,6 +184,13 @@ flag { } flag { + name: "support_multiple_labeledby" + namespace: "accessibility" + description: "Feature flag for supporting multiple labels in AccessibilityNodeInfo labeledby api" + bug: "333780959" +} + +flag { name: "support_system_pinch_zoom_opt_out_apis" namespace: "accessibility" description: "Feature flag for declaring system pinch zoom opt-out apis" diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 5b1c7d54ddb7..0ab51e45a951 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -563,7 +563,7 @@ public class AutofillFeatureFlags { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTOFILL, DEVICE_CONFIG_ENABLE_RELAYOUT, - true); + false); } /** @hide */ diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index c9d2eecfb9b0..fed8eea97688 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1352,12 +1352,16 @@ public final class InputMethodManager { case MSG_SET_VISIBILITY: final boolean visible = msg.arg1 != 0; synchronized (mH) { - if (visible) { - showSoftInput(mServedView, /* flags */ 0); - } else { - if (mCurRootView != null - && mCurRootView.getInsetsController() != null) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime()); + if (mCurRootView != null) { + final var insetsController = mCurRootView.getInsetsController(); + if (insetsController != null) { + if (visible) { + insetsController.show(WindowInsets.Type.ime(), + false /* fromIme */, null /* statsToken */); + } else { + insetsController.hide(WindowInsets.Type.ime(), + false /* fromIme */, null /* statsToken */); + } } } } @@ -2334,16 +2338,18 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { + final var viewRootImpl = view.getViewRootImpl(); // In case of a running show IME animation, it should not be requested visible, // otherwise the animation would jump and not be controlled by the user anymore - if ((mCurRootView.getInsetsController().computeUserAnimatingTypes() - & WindowInsets.Type.ime()) == 0) { + if (viewRootImpl != null + && (viewRootImpl.getInsetsController().computeUserAnimatingTypes() + & WindowInsets.Type.ime()) == 0) { // TODO(b/322992891) handle case of SHOW_IMPLICIT - view.getWindowInsetsController().show(WindowInsets.Type.ime()); + viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); return true; - } else { - return false; } + return false; } else { // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. // TODO(b/229426865): call WindowInsetsController#show instead. @@ -2497,7 +2503,10 @@ public final class InputMethodManager { if (Flags.refactorInsetsController()) { // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY - servedView.getWindowInsetsController().hide(WindowInsets.Type.ime()); + final var viewRootImpl = servedView.getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime()); + } return true; } else { return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index 4c8bad6d0aff..205f1defccd1 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -412,8 +412,7 @@ public class SnapshotDrawerUtils { final WindowManager.LayoutParams attrs = Flags.drawSnapshotAspectRatioMatch() ? info.mainWindowLayoutParams : info.topOpaqueWindowLayoutParams; final WindowManager.LayoutParams mainWindowParams = info.mainWindowLayoutParams; - final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; - if (attrs == null || mainWindowParams == null || topWindowInsetsState == null) { + if (attrs == null || mainWindowParams == null) { Log.w(TAG, "unable to create taskSnapshot surface "); return null; } @@ -456,7 +455,10 @@ public class SnapshotDrawerUtils { return layoutParams; } - static Rect getSystemBarInsets(Rect frame, InsetsState state) { + static Rect getSystemBarInsets(Rect frame, @Nullable InsetsState state) { + if (state == null) { + return new Rect(); + } return state.calculateInsets(frame, WindowInsets.Type.systemBars(), false /* ignoreVisibility */).toRect(); } diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 48fb2b3ab129..f739622a1b92 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -98,6 +98,13 @@ flag { } flag { + name: "scrolling_from_letterbox" + namespace: "large_screen_experiences_app_compat" + description: "Whether to enable app scrolling from gestures from letterbox area" + bug: "353697519" +} + +flag { name: "app_compat_refactoring" namespace: "large_screen_experiences_app_compat" description: "Whether the changes about app compat refactoring are enabled./n" diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 90245699c824..91ac4ff05687 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -101,6 +101,13 @@ flag { } flag { + name: "enable_cascading_windows" + namespace: "lse_desktop_experience" + description: "Whether to apply cascading effect for placing multiple windows when first launched" + bug: "325240051" +} + +flag { name: "enable_camera_compat_for_desktop_windowing" namespace: "lse_desktop_experience" description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 5397e91bd249..c451cc880a8c 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -19,6 +19,16 @@ flag { } flag { + name: "do_not_skip_ime_by_target_visibility" + namespace: "windowing_frontend" + description: "Avoid window traversal missing IME" + bug: "339375944" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "apply_lifecycle_on_pip_change" namespace: "windowing_frontend" description: "Make pip activity lifecyle change with windowing mode" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index ae9d757e1e0b..13d465f183c9 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -3,16 +3,6 @@ container: "system" # Project link: https://gantry.corp.google.com/projects/android_platform_windowing_sdk/changes -# Using a fixed read only flag because there are ClientTransaction scheduling before -# WindowManagerService creation. -flag { - namespace: "windowing_sdk" - name: "bundle_client_transaction_flag" - description: "To bundle multiple ClientTransactionItems into one ClientTransaction" - bug: "260873529" - is_fixed_read_only: true -} - flag { namespace: "windowing_sdk" name: "activity_embedding_overlay_presentation_flag" diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index 75ddb58590a1..f9c294758bf0 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -48,6 +48,7 @@ import android.os.Handler; import android.os.UserHandle; import android.os.Vibrator; import android.provider.Settings; +import android.provider.SettingsStringUtil; import android.speech.tts.TextToSpeech; import android.speech.tts.Voice; import android.text.TextUtils; @@ -151,7 +152,8 @@ public class AccessibilityShortcutController { * info for toggling a framework feature */ public static Map<ComponentName, FrameworkFeatureInfo> - getFrameworkShortcutFeaturesMap() { + getFrameworkShortcutFeaturesMap() { + if (sFrameworkShortcutFeaturesMap == null) { Map<ComponentName, FrameworkFeatureInfo> featuresMap = new ArrayMap<>(4); featuresMap.put(COLOR_INVERSION_COMPONENT_NAME, @@ -172,7 +174,7 @@ public class AccessibilityShortcutController { R.string.one_handed_mode_feature_name)); } featuresMap.put(REDUCE_BRIGHT_COLORS_COMPONENT_NAME, - new ToggleableFrameworkFeatureInfo( + new ExtraDimFrameworkFeatureInfo( Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, "1" /* Value to enable */, "0" /* Value to disable */, R.string.reduce_bright_colors_feature_name)); @@ -828,6 +830,44 @@ public class AccessibilityShortcutController { } } + + public static class ExtraDimFrameworkFeatureInfo extends FrameworkFeatureInfo { + ExtraDimFrameworkFeatureInfo(String settingKey, String settingOnValue, + String settingOffValue, int labelStringResourceId) { + super(settingKey, settingOnValue, settingOffValue, labelStringResourceId); + } + + /** + * Perform shortcut action. + * + * @return True if the accessibility service is enabled, false otherwise. + */ + public boolean activateShortcut(Context context, int userId) { + if (com.android.server.display.feature.flags.Flags.evenDimmer() + && context.getResources().getBoolean( + com.android.internal.R.bool.config_evenDimmerEnabled)) { + launchExtraDimDialog(); + return true; + } else { + // Assuming that the default state will be to have the feature off + final SettingsStringUtil.SettingStringHelper + setting = new SettingsStringUtil.SettingStringHelper( + context.getContentResolver(), getSettingKey(), userId); + if (!TextUtils.equals(getSettingOnValue(), setting.read())) { + setting.write(getSettingOnValue()); + return true; + } else { + setting.write(getSettingOffValue()); + return false; + } + } + } + + private void launchExtraDimDialog() { + // TODO: launch Extra dim dialog for feature migration + } + } + // Class to allow mocking of static framework calls public static class FrameworkObjectProvider { public AccessibilityManager getAccessibilityManagerInstance(Context context) { diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 3e6f18e8e24b..69d1cb34005d 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -161,12 +161,12 @@ public class Cuj { public static final int CUJ_DESKTOP_MODE_RESIZE_WINDOW = 106; /** - * Track entering desktop mode interaction via app handle drag. + * Track app handle drag and hold interaction. * * <p>Tracking starts when the app handle is dragged and - * finishes when the window animation to desktop ends after app handle release. + * finishes immediately after app handle release, before starting a new transition. */ - public static final int CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG = 107; + public static final int CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD = 107; /** Track exiting desktop mode interaction. */ public static final int CUJ_DESKTOP_MODE_EXIT_MODE = 108; @@ -197,8 +197,21 @@ public class Cuj { /** Track launching an app through the Launcher Keyboard Quick Switch View */ public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH = 115; + /** + * Track entering desktop mode interaction via app handle drag release. + * + * <p>Tracking starts when the app handle is released and + * finishes when one of the three possible animations end: + * <ul> + * <li>release to desktop</li> + * <li>release to split-screen</li> + * <li>release to back to full-screen</li> + * </ul>. + */ + public static final int CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE = 116; + // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. - @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; + @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE; /** @hide */ @IntDef({ @@ -297,7 +310,7 @@ public class Cuj { CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, CUJ_FOLD_ANIM, CUJ_DESKTOP_MODE_RESIZE_WINDOW, - CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG, + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD, CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU, CUJ_DESKTOP_MODE_EXIT_MODE, CUJ_DESKTOP_MODE_MINIMIZE_WINDOW, @@ -305,7 +318,8 @@ public class Cuj { CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN, CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE, - CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH + CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH, + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE }) @Retention(RetentionPolicy.SOURCE) public @interface CujType {} @@ -414,7 +428,7 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MAXIMIZE_WINDOW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_FOLD_ANIM] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__FOLD_ANIM; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_RESIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_RESIZE_WINDOW; - CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MINIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MINIMIZE_WINDOW; @@ -423,6 +437,7 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE; } private Cuj() { @@ -631,8 +646,8 @@ public class Cuj { return "FOLD_ANIM"; case CUJ_DESKTOP_MODE_RESIZE_WINDOW: return "DESKTOP_MODE_RESIZE_WINDOW"; - case CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG: - return "DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG"; + case CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD: + return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD"; case CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU: return "DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU"; case CUJ_DESKTOP_MODE_EXIT_MODE: @@ -649,6 +664,8 @@ public class Cuj { return "LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE"; case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH: return "LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH"; + case CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE: + return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java index 24971f51aabf..488e06f9a1ad 100644 --- a/core/java/com/android/internal/os/PowerStats.java +++ b/core/java/com/android/internal/os/PowerStats.java @@ -580,10 +580,15 @@ public final class PowerStats { } PowerStatsFormatter uidStatsFormatter = descriptor.getUidStatsFormatter(); for (int i = 0; i < uidStats.size(); i++) { + String formattedStats = uidStatsFormatter.format(uidStats.valueAt(i)); + if (formattedStats.isBlank()) { + continue; + } + pw.print("UID "); pw.print(UserHandle.formatUid(uidStats.keyAt(i))); pw.print(": "); - pw.print(uidStatsFormatter.format(uidStats.valueAt(i))); + pw.print(formattedStats); pw.println(); } pw.decreaseIndent(); diff --git a/core/java/com/android/internal/os/TEST_MAPPING b/core/java/com/android/internal/os/TEST_MAPPING index d552e0b8c643..ae43acfdedb6 100644 --- a/core/java/com/android/internal/os/TEST_MAPPING +++ b/core/java/com/android/internal/os/TEST_MAPPING @@ -18,7 +18,7 @@ "Kernel[^/]*\\.java", "[^/]*Power[^/]*\\.java" ], - "name": "BatteryUsageStatsProtoTests" + "name": "PowerStatsTests" }, { "file_patterns": [ diff --git a/core/java/com/android/internal/policy/FoldLockSettingsObserver.java b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java new file mode 100644 index 000000000000..c6fba8a31277 --- /dev/null +++ b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java @@ -0,0 +1,128 @@ +/* + * 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.internal.policy; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.Set; + +/** + * A ContentObserver that listens for changes in the "Continue using apps on fold" setting. This + * setting determines a device's behavior when the user folds the device. + * @hide + * + * Keep the setting values in this class in sync with the values in + * {@link com.android.server.utils.FoldSettingProvider} and + * {@link com.android.settings.display.FoldLockBehaviorSettings} + */ +public class FoldLockSettingsObserver extends ContentObserver { + /** The setting for "stay awake on fold". */ + public static final String SETTING_VALUE_STAY_AWAKE_ON_FOLD = "stay_awake_on_fold_key"; + /** The setting for "swipe up to continue". */ + public static final String SETTING_VALUE_SELECTIVE_STAY_AWAKE = "selective_stay_awake_key"; + /** The setting for "always sleep on fold". */ + public static final String SETTING_VALUE_SLEEP_ON_FOLD = "sleep_on_fold_key"; + public static final String SETTING_VALUE_DEFAULT = SETTING_VALUE_SELECTIVE_STAY_AWAKE; + private static final Set<String> SETTING_VALUES = Set.of(SETTING_VALUE_STAY_AWAKE_ON_FOLD, + SETTING_VALUE_SELECTIVE_STAY_AWAKE, SETTING_VALUE_SLEEP_ON_FOLD); + + private final Context mContext; + + /** The cached value of the setting. */ + @VisibleForTesting + String mFoldLockSetting; + + public FoldLockSettingsObserver(Handler handler, Context context) { + super(handler); + mContext = context; + } + + /** Registers the observer and updates the cache for the first time. */ + public void register() { + final ContentResolver r = mContext.getContentResolver(); + r.registerContentObserver( + Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR), + false, this, UserHandle.USER_ALL); + requestAndCacheFoldLockSetting(); + } + + /** Unregisters the observer. */ + public void unregister() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + /** Runs when settings changes. */ + @Override + public void onChange(boolean selfChange) { + requestAndCacheFoldLockSetting(); + } + + /** + * Requests and caches the current FOLD_LOCK_BEHAVIOR setting, which should be one of three + * values: SETTING_VALUE_STAY_AWAKE_ON_FOLD, SETTING_VALUE_SELECTIVE_STAY_AWAKE, + * SETTING_VALUE_SLEEP_ON_FOLD. If null (not set), returns the system default. + */ + @VisibleForTesting + void requestAndCacheFoldLockSetting() { + String currentSetting = request(); + + if (currentSetting == null || !SETTING_VALUES.contains(currentSetting)) { + currentSetting = SETTING_VALUE_DEFAULT; + } + + setCurrentFoldSetting(currentSetting); + } + + /** + * Makes a binder call to request the current FOLD_LOCK_BEHAVIOR setting. + */ + @VisibleForTesting + @Nullable + String request() { + return Settings.System.getStringForUser(mContext.getContentResolver(), + Settings.System.FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT); + } + + /** Caches the fold-lock behavior received from Settings. */ + @VisibleForTesting + void setCurrentFoldSetting(String newSetting) { + mFoldLockSetting = newSetting; + } + + /** Used by external requesters: checks if the current setting is "stay awake on fold". */ + public boolean isStayAwakeOnFold() { + return mFoldLockSetting.equals(SETTING_VALUE_STAY_AWAKE_ON_FOLD); + } + + /** Used by external requesters: checks if the current setting is "swipe up to continue". */ + public boolean isSelectiveStayAwake() { + return mFoldLockSetting.equals(SETTING_VALUE_SELECTIVE_STAY_AWAKE); + } + + /** Used by external requesters: checks if the current setting is "sleep on fold". */ + public boolean isSleepOnFold() { + return mFoldLockSetting.equals(SETTING_VALUE_SLEEP_ON_FOLD); + } +} diff --git a/core/java/com/android/internal/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl new file mode 100644 index 000000000000..969ed99d3aca --- /dev/null +++ b/core/java/com/android/internal/protolog/IProtoLogClient.aidl @@ -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.internal.protolog; + +/** + * The ProtoLog client interface. + * + * These clients will communicate bi-directionally with the ProtoLog service + * (@see IProtoLogService.aidl) running in the system process. + * + * {@hide} + */ +interface IProtoLogClient { + void toggleLogcat(boolean enabled, in String[] groups); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/protolog/IProtoLogService.aidl b/core/java/com/android/internal/protolog/IProtoLogService.aidl new file mode 100644 index 000000000000..cc349ea2985a --- /dev/null +++ b/core/java/com/android/internal/protolog/IProtoLogService.aidl @@ -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.internal.protolog; + +import com.android.internal.protolog.IProtoLogClient; + +/** + * The ProtoLog Service interface. + * + * This service runs in system server. + * + * All ProtoLog clients (in theory one per process) will register themselves to this service. + * This service will then serve as the entry point for any shell command based interactions with + * protolog and get/forward any required information/actions from/to all the registered ProtoLog + * clients. + * + * Clients will be responsible for directly sending all their trace data to Perfetto without passing + * through this service, except viewer config data, the mappings from messages hashes (generated by + * the ProtoLog Tool if and when the tool processed the source code). So, this service is + * responsible for dumping the viewer config data from all clients. The reason for this is because + * we do this on Perfetto's onFlush callback when a tracing instance is stopped. However, if a + * client process is frozen then this will not dump; system server, where this service runs cannot + * be frozen. Secondly many processes (i.e. apps) will run the same code with the same viewer config + * data, so this service allows us to orchestrate which config gets dumped so we don't duplicate + * this information in the trace and waste valuable trace space. + * + * {@hide} + */ +interface IProtoLogService { + interface IRegisterClientArgs { + String[] getGroups(); + boolean[] getGroupsDefaultLogcatStatus(); + String getViewerConfigFile(); + } + + void registerClient(IProtoLogClient client, IRegisterClientArgs args); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 652cba7ed00d..fbec1f104fc8 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -88,7 +88,7 @@ import java.util.concurrent.locks.ReentrantLock; /** * A service for the ProtoLog logging system. */ -public class PerfettoProtoLogImpl implements IProtoLog { +public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog { private static final String LOG_TAG = "ProtoLog"; public static final String NULL_STRING = "null"; private final AtomicInteger mTracingInstances = new AtomicInteger(); @@ -100,6 +100,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { ); @Nullable private final ProtoLogViewerConfigReader mViewerConfigReader; + @Nullable private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); private final Runnable mCacheUpdater; @@ -111,13 +112,12 @@ public class PerfettoProtoLogImpl implements IProtoLog { private final Lock mBackgroundServiceLock = new ReentrantLock(); private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) { + public PerfettoProtoLogImpl(@NonNull String viewerConfigFilePath, Runnable cacheUpdater) { this(() -> { try { return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); } catch (FileNotFoundException e) { - Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e); - return null; + throw new RuntimeException("Failed to load viewer config file " + viewerConfigFilePath, e); } }, cacheUpdater); } @@ -127,7 +127,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { } public PerfettoProtoLogImpl( - @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, Runnable cacheUpdater ) { this(viewerConfigInputStreamProvider, @@ -203,13 +203,23 @@ public class PerfettoProtoLogImpl implements IProtoLog { return mTracingInstances.get() > 0; } + @Override + public void toggleLogcat(boolean enabled, String[] groups) { + final ILogger logger = (message) -> Log.d(LOG_TAG, message); + if (enabled) { + startLoggingToLogcat(groups, logger); + } else { + stopLoggingToLogcat(groups, logger); + } + } + /** * Start text logging * @param groups Groups to start text logging for * @param logger A logger to write status updates to * @return status code */ - public int startLoggingToLogcat(String[] groups, ILogger logger) { + public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) { if (mViewerConfigReader != null) { mViewerConfigReader.loadViewerConfig(groups, logger); } @@ -222,7 +232,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { * @param logger A logger to write status updates to * @return status code */ - public int stopLoggingToLogcat(String[] groups, ILogger logger) { + public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) { if (mViewerConfigReader != null) { mViewerConfigReader.unloadViewerConfig(groups, logger); } @@ -242,13 +252,29 @@ public class PerfettoProtoLogImpl implements IProtoLog { for (IProtoLogGroup protoLogGroup : protoLogGroups) { mLogGroups.put(protoLogGroup.name(), protoLogGroup); } + + final String[] groupsLoggingToLogcat = Arrays.stream(protoLogGroups) + .filter(IProtoLogGroup::isLogToLogcat) + .map(IProtoLogGroup::name) + .toArray(String[]::new); + + if (mViewerConfigReader != null) { + mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat); + } } /** * Responds to a shell command. */ + @Deprecated public int onShellCommand(ShellCommand shell) { PrintWriter pw = shell.getOutPrintWriter(); + + if (android.tracing.Flags.clientSideProtoLogging()) { + pw.println("Command deprecated. Please use 'cmd protolog' instead."); + return -1; + } + String cmd = shell.getNextArg(); if (cmd == null) { return unknownCommand(pw); @@ -742,7 +768,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { return -1; } - private synchronized void onTracingInstanceStart(ProtoLogDataSource.ProtoLogConfig config) { + private synchronized void onTracingInstanceStart( + int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { mDefaultLogLevelCounts[i]++; @@ -775,7 +802,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { this.mTracingInstances.incrementAndGet(); } - private synchronized void onTracingInstanceStop(ProtoLogDataSource.ProtoLogConfig config) { + private synchronized void onTracingInstanceStop( + int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { this.mTracingInstances.decrementAndGet(); final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index 84f3237142b8..6dc6585b5caf 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -49,12 +49,12 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState, ProtoLogDataSource.IncrementalState> { - private final Consumer<ProtoLogConfig> mOnStart; + private final Instance.TracingInstanceStartCallback mOnStart; private final Runnable mOnFlush; - private final Consumer<ProtoLogConfig> mOnStop; + private final Instance.TracingInstanceStopCallback mOnStop; - public ProtoLogDataSource(Consumer<ProtoLogConfig> onStart, Runnable onFlush, - Consumer<ProtoLogConfig> onStop) { + public ProtoLogDataSource(Instance.TracingInstanceStartCallback onStart, Runnable onFlush, + Instance.TracingInstanceStopCallback onStop) { super("android.protolog"); this.mOnStart = onStart; this.mOnFlush = onFlush; @@ -267,20 +267,30 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, public static class Instance extends DataSourceInstance { - private final Consumer<ProtoLogConfig> mOnStart; + public interface TracingInstanceStartCallback { + void run(int instanceIdx, ProtoLogConfig config); + } + + public interface TracingInstanceStopCallback { + void run(int instanceIdx, ProtoLogConfig config); + } + + private final TracingInstanceStartCallback mOnStart; private final Runnable mOnFlush; - private final Consumer<ProtoLogConfig> mOnStop; + private final TracingInstanceStopCallback mOnStop; private final ProtoLogConfig mConfig; + private final int mInstanceIndex; public Instance( DataSource<Instance, TlsState, IncrementalState> dataSource, int instanceIdx, ProtoLogConfig config, - Consumer<ProtoLogConfig> onStart, + TracingInstanceStartCallback onStart, Runnable onFlush, - Consumer<ProtoLogConfig> onStop + TracingInstanceStopCallback onStop ) { super(dataSource, instanceIdx); + this.mInstanceIndex = instanceIdx; this.mOnStart = onStart; this.mOnFlush = onFlush; this.mOnStop = onStop; @@ -289,7 +299,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, @Override public void onStart(StartCallbackArguments args) { - this.mOnStart.accept(this.mConfig); + this.mOnStart.run(this.mInstanceIndex, this.mConfig); } @Override @@ -299,7 +309,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, @Override public void onStop(StopCallbackArguments args) { - this.mOnStop.accept(this.mConfig); + this.mOnStop.run(this.mInstanceIndex, this.mConfig); } } } diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 3082295a522c..77ca7ce91b22 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -30,6 +30,7 @@ import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLogToolInjected; +import java.io.File; import java.util.TreeMap; /** @@ -105,7 +106,15 @@ public class ProtoLogImpl { public static synchronized IProtoLog getSingleInstance() { if (sServiceInstance == null) { if (android.tracing.Flags.perfettoProtologTracing()) { - sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + File f = new File(sViewerConfigPath); + if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) { + // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 + // In so tests the viewer config file might not exist in which we don't + // want to provide config path to the user + sServiceInstance = new PerfettoProtoLogImpl(null, null, sCacheUpdater); + } else { + sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + } } else { sServiceInstance = new LegacyProtoLogImpl( sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater); diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index bb6c8b7a9698..38ca0d8f75e8 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -24,12 +24,13 @@ import java.util.Set; import java.util.TreeMap; public class ProtoLogViewerConfigReader { + @NonNull private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>(); private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>(); public ProtoLogViewerConfigReader( - ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; } diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java index 334f5488425a..14bc8e4782f2 100644 --- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java +++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java @@ -16,11 +16,13 @@ package com.android.internal.protolog; +import android.annotation.NonNull; import android.util.proto.ProtoInputStream; public interface ViewerConfigInputStreamProvider { /** * @return a ProtoInputStream. */ + @NonNull ProtoInputStream getInputStream(); } diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index c07fd3838837..7c62615cdc42 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -27,6 +27,7 @@ #include <android_media_audiopolicy.h> #include <android_os_Parcel.h> #include <audiomanager/AudioManager.h> +#include <android-base/properties.h> #include <binder/IBinder.h> #include <jni.h> #include <media/AidlConversion.h> @@ -41,8 +42,10 @@ #include <system/audio_policy.h> #include <utils/Log.h> +#include <thread> #include <optional> #include <sstream> +#include <memory> #include <vector> #include "android_media_AudioAttributes.h" @@ -261,6 +264,13 @@ static struct { jfieldID mMixerBehavior; } gAudioMixerAttributesField; +static struct { + jclass clazz; + jmethodID run; +} gRunnableClassInfo; + +static JavaVM* gVm; + static Mutex gLock; enum AudioError { @@ -3362,6 +3372,55 @@ static jboolean android_media_AudioSystem_isBluetoothVariableLatencyEnabled(JNIE return enabled; } +class JavaSystemPropertyListener { + public: + JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) : + mCallback(env->NewGlobalRef(javaCallback)), + mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) { + mListenerThread = std::thread([this]() mutable { + JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm); + while (!mCleanupSignal.load()) { + using namespace std::chrono_literals; + // 1s timeout so this thread can read the cleanup signal to (slowly) be able to + // be destroyed. + std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: ""; + if (newVal != "" && mLastVal != newVal) { + threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run); + mLastVal = std::move(newVal); + } + } + }); + } + + ~JavaSystemPropertyListener() { + mCleanupSignal.store(true); + mListenerThread.join(); + JNIEnv* env = GetOrAttachJNIEnvironment(gVm); + env->DeleteGlobalRef(mCallback); + } + + private: + jobject mCallback; + android::base::CachedProperty mCachedProperty; + std::thread mListenerThread; + std::atomic<bool> mCleanupSignal{false}; + std::string mLastVal = ""; +}; + +std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners; +std::mutex gSysPropLock{}; + +static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env, jobject thiz, + jstring sysProp, + jobject javaCallback) { + ScopedUtfChars sysPropChars{env, sysProp}; + auto listener = std::make_unique<JavaSystemPropertyListener>(env, javaCallback, + std::string{sysPropChars.c_str()}); + std::unique_lock _l{gSysPropLock}; + gSystemPropertyListeners.push_back(std::move(listener)); +} + + // ---------------------------------------------------------------------------- #define MAKE_AUDIO_SYSTEM_METHOD(x) \ @@ -3534,7 +3593,12 @@ static const JNINativeMethod gMethods[] = android_media_AudioSystem_clearPreferredMixerAttributes), MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency), MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled), - MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled)}; + MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled), + MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange", + "(Ljava/lang/String;Ljava/lang/Runnable;)V", + android_media_AudioSystem_listenForSystemPropertyChange), + + }; static const JNINativeMethod gEventHandlerMethods[] = {MAKE_JNI_NATIVE_METHOD("native_setup", "(Ljava/lang/Object;)V", @@ -3816,6 +3880,12 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixerAttributesField.mMixerBehavior = GetFieldIDOrDie(env, audioMixerAttributesClass, "mMixerBehavior", "I"); + jclass runnableClazz = FindClassOrDie(env, "java/lang/Runnable"); + gRunnableClassInfo.clazz = MakeGlobalRefOrDie(env, runnableClazz); + gRunnableClassInfo.run = GetMethodIDOrDie(env, runnableClazz, "run", "()V"); + + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&gVm) != 0); + AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback); RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index d32486c73db2..240be3fe5534 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -415,7 +415,7 @@ static void android_view_MotionEvent_nativeAddBatch(JNIEnv* env, jclass clazz, env->DeleteLocalRef(pointerCoordsObj); } - event->addSample(eventTimeNanos, rawPointerCoords.data()); + event->addSample(eventTimeNanos, rawPointerCoords.data(), event->getId()); event->setMetaState(event->getMetaState() | metaState); } diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 7e2a5ace7e64..020b27e82bea 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -47,13 +47,12 @@ using namespace std; */ extern int register_android_os_Binder(JNIEnv* env); -extern int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env); +extern int register_libcore_util_NativeAllocationRegistry(JNIEnv* env); typedef void (*FreeFunction)(void*); -static void NativeAllocationRegistry_Delegate_nativeApplyFreeFunction(JNIEnv*, jclass, - jlong freeFunction, - jlong ptr) { +static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*, jclass, jlong freeFunction, + jlong ptr) { void* nativePtr = reinterpret_cast<void*>(static_cast<uintptr_t>(ptr)); FreeFunction nativeFreeFunction = reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction)); @@ -61,11 +60,11 @@ static void NativeAllocationRegistry_Delegate_nativeApplyFreeFunction(JNIEnv*, j } static JNINativeMethod gMethods[] = { - NATIVE_METHOD(NativeAllocationRegistry_Delegate, nativeApplyFreeFunction, "(JJ)V"), + NATIVE_METHOD(NativeAllocationRegistry, applyFreeFunction, "(JJ)V"), }; -int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env) { - return jniRegisterNativeMethods(env, "libcore/util/NativeAllocationRegistry_Delegate", gMethods, +int register_libcore_util_NativeAllocationRegistry(JNIEnv* env) { + return jniRegisterNativeMethods(env, "libcore/util/NativeAllocationRegistry", gMethods, NELEM(gMethods)); } @@ -147,8 +146,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.view.VelocityTracker", REG_JNI(register_android_view_VelocityTracker)}, {"com.android.internal.util.VirtualRefBasePtr", REG_JNI(register_com_android_internal_util_VirtualRefBasePtr)}, - {"libcore.util.NativeAllocationRegistry_Delegate", - REG_JNI(register_libcore_util_NativeAllocationRegistry_Delegate)}, + {"libcore.util.NativeAllocationRegistry", + REG_JNI(register_libcore_util_NativeAllocationRegistry)}, }; static int register_jni_procs(const std::unordered_map<std::string, RegJNIRec>& jniRegMap, diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index 5a4d6dbdc085..12804d43b04e 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -147,6 +147,7 @@ message VibrationProto { IGNORED_ON_WIRELESS_CHARGER = 27; IGNORED_MISSING_PERMISSION = 28; CANCELLED_BY_APP_OPS = 29; + CANCELLED_BY_FOREGROUND_USER = 30; reserved 17; // prev IGNORED_UNKNOWN_VIBRATION } } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index dc3d9355f148..236e7c5b7a62 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7090,4 +7090,7 @@ <!-- Whether to show GAIA education screen during account login of private space setup. OEM/Partner can explicitly opt to disable the screen. --> <bool name="config_enableGaiaEducationInPrivateSpace">true</bool> + + <!-- Whether to enable usb state update via udc sysfs. --> + <bool name="config_enableUdcSysfsUsbStateUpdate">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fcafdaed8d1a..09688f2f7bec 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -523,6 +523,7 @@ <java-symbol type="integer" name="config_defaultAnalogClockSecondsHandFps"/> <java-symbol type="bool" name="config_notificationCloseButtonSupported"/> <java-symbol type="bool" name="config_enableGaiaEducationInPrivateSpace"/> + <java-symbol type="bool" name="config_enableUdcSysfsUsbStateUpdate"/> <java-symbol type="color" name="tab_indicator_text_v4" /> diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java index 9d7d71d8d539..616c72ec6078 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java @@ -20,8 +20,8 @@ import static org.junit.Assert.assertEquals; import android.graphics.Matrix; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java index d7b911dda672..d0a4141eb9ed 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java index 4839dd27b283..013117e89c3e 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java @@ -45,8 +45,8 @@ import android.util.StringBuilderPrinter; import android.view.MotionEvent; import android.view.autofill.AutofillId; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index ce85a76f478d..61bf1378fc36 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -28,9 +28,9 @@ import android.os.Bundle; import android.os.Parcel; import android.platform.test.flag.junit.SetFlagsRule; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.frameworks.inputmethodcoretests.R; @@ -135,7 +135,7 @@ public class InputMethodInfoTest { private InputMethodInfo buildInputMethodForTest(final @XmlRes int metaDataRes) throws Exception { - final Context context = InstrumentationRegistry.getContext(); + final Context context = InstrumentationRegistry.getInstrumentation().getContext(); final ServiceInfo serviceInfo = new ServiceInfo(); serviceInfo.applicationInfo = context.getApplicationInfo(); serviceInfo.packageName = context.getPackageName(); diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java index d70572444128..812b3f5cd153 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java @@ -24,9 +24,9 @@ import static org.junit.Assert.assertNotNull; import android.content.Context; import android.hardware.display.DisplayManager; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java index e7b1110f898a..73ff30408a7b 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java @@ -26,8 +26,8 @@ import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java index 5095cad1b607..4c76992cbcac 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java @@ -27,8 +27,8 @@ import android.annotation.Nullable; import android.os.Parcel; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java index 47a724d36038..608dd4d9fca1 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.PointF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java index a94f8772fcd0..bb6a944ba17f 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java @@ -24,8 +24,8 @@ import android.os.CancellationSignal; import android.os.CancellationSignalBeamer; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java index b2eb07c0a9e7..4cbd7ab1ef65 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java index df63a4aaaefe..c1e2197b8d85 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java index f264cc630dc5..724d729c950f 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java @@ -25,8 +25,8 @@ import android.graphics.RectF; import android.os.Parcel; import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java index a6262944e8b0..9eb5dd1a2422 100644 --- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java @@ -22,8 +22,8 @@ import android.platform.test.annotations.Presubmit; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionWrapper; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java index 32bfdcb7b217..6ac36398ad55 100644 --- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java @@ -20,8 +20,8 @@ import static org.junit.Assert.assertEquals; import android.view.WindowManager.LayoutParams; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java index ba6390808151..7b0a5dacbea0 100644 --- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java @@ -18,8 +18,8 @@ package com.android.internal.inputmethod; import static org.junit.Assert.assertEquals; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp deleted file mode 100644 index 1fb5f2c0789b..000000000000 --- a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp +++ /dev/null @@ -1,30 +0,0 @@ -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], -} - -android_test { - name: "BatteryUsageStatsProtoTests", - srcs: ["src/**/*.java"], - - static_libs: [ - "androidx.test.rules", - "junit", - "mockito-target-minus-junit4", - "platform-test-annotations", - "platformprotosnano", - "statsdprotolite", - "truth", - ], - - libs: ["android.test.runner"], - - platform_apis: true, - certificate: "platform", - - test_suites: ["device-tests"], -} diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java index cb3f99c37a4f..33a46d0fde17 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java @@ -35,6 +35,8 @@ import org.junit.Test; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + @MediumTest public class AnimatorSetCallsTest { @@ -447,6 +449,43 @@ public class AnimatorSetCallsTest { mActivity.runOnUiThread(() -> {}); } + @Test + public void startAfterSeek() throws Throwable { + ArrayList<Float> values = new ArrayList<>(); + AtomicReference<CountDownLatch> drawLatch = new AtomicReference<>(new CountDownLatch(1)); + + mActivity.runOnUiThread(() -> { + mAnimator.setDuration(300); + mAnimator.setInterpolator(null); + View view = (View) mAnimator.getTarget(); + view.getViewTreeObserver().addOnDrawListener(() -> { + values.add(view.getTranslationX()); + drawLatch.get().countDown(); + }); + mSet1.setCurrentPlayTime(150); + }); + + assertTrue(drawLatch.get().await(1, TimeUnit.SECONDS)); + drawLatch.set(new CountDownLatch(1)); + + mActivity.runOnUiThread(() -> { + assertEquals(1, values.size()); + assertEquals(50f, values.get(0), 0.01f); + mSet1.start(); + }); + + assertTrue(drawLatch.get().await(1, TimeUnit.SECONDS)); + + mActivity.runOnUiThread(() -> { + assertTrue(values.size() >= 2); + float lastValue = values.get(0); + for (int i = 1; i < values.size(); i++) { + assertTrue(values.get(i) >= lastValue); + lastValue = values.get(i); + } + }); + } + private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) { final boolean[] value = new boolean[1]; PollingCheck.waitFor(() -> { diff --git a/core/tests/coretests/src/android/animation/OWNERS b/core/tests/coretests/src/android/animation/OWNERS new file mode 100644 index 000000000000..1eefb3a3dc65 --- /dev/null +++ b/core/tests/coretests/src/android/animation/OWNERS @@ -0,0 +1 @@ +include /core/java/android/animation/OWNERS diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index e8a0762f70c0..294352e7f928 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -90,7 +90,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; @@ -117,6 +118,9 @@ public class ActivityThreadTest { // few sequence numbers the framework used to launch the test activity. private static final int BASE_SEQ = 10000000; + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule(order = 0) public final ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */, @@ -133,8 +137,6 @@ public class ActivityThreadTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); - // Keep track of the original controller, so that it can be used to restore in tearDown() // when there is override in some test cases. mOriginalWindowTokenClientController = WindowTokenClientController.getInstance(); diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index c7060adc1ca1..72c4639277bb 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -47,10 +47,12 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Tests for subtypes of {@link ClientTransactionItem}. @@ -63,6 +65,9 @@ import org.mockito.MockitoAnnotations; @Presubmit public class ClientTransactionItemTest { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + @Mock private ClientTransactionHandler mHandler; @Mock @@ -89,7 +94,6 @@ public class ClientTransactionItemTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mGlobalConfig = new Configuration(); mConfiguration = new Configuration(); mActivitiesToBeDestroyed = new ArrayMap<>(); diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java index d2a444f0d0df..f9609fcad8a1 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java @@ -59,7 +59,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.concurrent.RejectedExecutionException; import java.util.function.BiConsumer; @@ -76,6 +77,8 @@ import java.util.function.BiConsumer; public class ClientTransactionListenerControllerTest { @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @Mock @@ -100,7 +103,6 @@ public class ClientTransactionListenerControllerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mDisplayManager = new DisplayManagerGlobal(mIDisplayManager); mHandler = getInstrumentation().getContext().getMainThreadHandler(); mController = spy(ClientTransactionListenerController diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java index 32e611cf0a1e..e429cfc687bb 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java @@ -21,12 +21,7 @@ import static android.app.servertransaction.TestUtils.mergedConfig; import static android.app.servertransaction.TestUtils.referrerIntentList; import static android.app.servertransaction.TestUtils.resultInfoList; -import static com.android.window.flags.Flags.FLAG_DISABLE_OBJECT_POOL; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; import android.annotation.NonNull; import android.app.ActivityOptions; @@ -41,27 +36,20 @@ import android.os.Bundle; import android.os.IBinder; import android.os.PersistableBundle; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.FlagsParameterization; -import android.platform.test.flag.junit.SetFlagsRule; import android.window.ActivityWindowInfo; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -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.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; -import java.util.List; import java.util.function.Supplier; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - /** * Tests for {@link ObjectPool}. * @@ -71,33 +59,19 @@ import platform.test.runner.parameterized.Parameters; * <p>This test class is a part of Window Manager Service tests and specified in * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}. */ -@RunWith(ParameterizedAndroidJunit4.class) +@RunWith(AndroidJUnit4.class) @SmallTest @Presubmit public class ObjectPoolTests { - @Parameters(name = "{0}") - public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf(FLAG_DISABLE_OBJECT_POOL); - } - @Rule - public SetFlagsRule mSetFlagsRule; + public final MockitoRule mocks = MockitoJUnit.rule(); @Mock private IApplicationThread mApplicationThread; @Mock private IBinder mActivityToken; - public ObjectPoolTests(FlagsParameterization flags) { - mSetFlagsRule = new SetFlagsRule(flags); - } - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - // 1. Check if two obtained objects from pool are not the same. // 2. Check if the state of the object is cleared after recycling. // 3. Check if the same object is obtained from pool after recycling. @@ -221,30 +195,11 @@ public class ObjectPoolTests { item.recycle(); final ObjectPoolItem item2 = obtain.get(); - if (Flags.disableObjectPool()) { - assertNotSame(item, item2); // Different instance. - } else { - assertSame(item, item2); - } + assertNotSame(item, item2); // Different instance. // Create new object when the pool is empty. final ObjectPoolItem item3 = obtain.get(); assertNotSame(item, item3); - if (Flags.disableObjectPool()) { - // Skip recycle if flag enabled, compare unnecessary. - return; - } - assertEquals(item, item3); - - // Reset fields after recycle. - item.recycle(); - - assertNotEquals(item, item3); - - // Recycled objects are equal. - item3.recycle(); - - assertEquals(item, item3); } } diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java index 73b7447a31c0..eb69b9c8b6e1 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java @@ -57,11 +57,13 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.Arrays; import java.util.Collections; @@ -83,6 +85,9 @@ import java.util.stream.Collectors; @Presubmit public class TransactionExecutorTests { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + @Mock private ClientTransactionHandler mTransactionHandler; @Mock @@ -98,8 +103,6 @@ public class TransactionExecutorTests { @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mClientRecord = new ActivityClientRecord(); when(mTransactionHandler.getActivityClient(any())).thenReturn(mClientRecord); diff --git a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java index 6998c32978c1..f5e9cc6953fb 100644 --- a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java +++ b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java @@ -26,8 +26,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.platform.test.annotations.RequiresFlagsEnabled; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java index 71c068d7ad46..9750de386271 100644 --- a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java +++ b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java @@ -35,6 +35,7 @@ import static android.text.format.DateUtils.FORMAT_SHOW_YEAR; import static android.text.format.DateUtils.FORMAT_UTC; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import android.icu.util.Calendar; import android.icu.util.TimeZone; @@ -683,4 +684,12 @@ public class DateIntervalFormatTest { assertEquals("February 27\u2009\u2013\u2009March 1, 2004", fmt.apply(1077840000000L, 1078185600000L)); } + + @Test + public void testIsLibcoreVFlagEnabled() { + // This flag has been fully ramped. It should never be false. + assertTrue(DateIntervalFormat.isLibcoreVFlagEnabled()); + // Call a Android V API in libcore. + assertEquals("\ud840\udc2b", Character.toString(0x2002B)); + } } diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java index 47b288993d11..248db65d7435 100644 --- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java @@ -37,8 +37,12 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.widget.TextView; @@ -46,6 +50,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -61,6 +66,9 @@ import org.mockito.Spy; @RunWith(AndroidJUnit4.class) public class ImeInsetsSourceConsumerTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); InsetsSourceConsumer mImeConsumer; @Spy InsetsController mController; @@ -112,6 +120,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeRequestedVisibleAwaitingControl() { // Set null control and then request show. mController.onControlsChanged(new InsetsSourceControl[] { null }); @@ -141,6 +150,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeRequestedVisibleAwaitingLeash() { // Set null control, then request show. mController.onControlsChanged(new InsetsSourceControl[] { null }); @@ -185,6 +195,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeGetAndClearSkipAnimationOnce_expectSkip() { // Expect IME animation will skipped when the IME is visible at first place. verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */, @@ -192,6 +203,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() { // Expect IME animation will not skipped if previously no view focused when gained the // window focus and requesting the IME visible next time. diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index b68ff78107ca..62291d405a60 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -237,8 +237,8 @@ public class ViewFrameRateTest { return; } waitForFrameRateCategoryToSettle(); - assertEquals(FRAME_RATE_CATEGORY_LOW, - mViewRoot.getLastPreferredFrameRateCategory()); + assertTrue(mViewRoot.getLastPreferredFrameRateCategory() + < FRAME_RATE_CATEGORY_HIGH_HINT); int width = mMovingView.getWidth(); int height = mMovingView.getHeight(); diff --git a/core/tests/coretests/src/android/view/WindowInfoTest.java b/core/tests/coretests/src/android/view/WindowInfoTest.java index d927f0632273..43e678fb634c 100644 --- a/core/tests/coretests/src/android/view/WindowInfoTest.java +++ b/core/tests/coretests/src/android/view/WindowInfoTest.java @@ -34,8 +34,8 @@ import android.platform.test.annotations.Presubmit; import android.text.TextUtils; import android.view.accessibility.AccessibilityNodeInfo; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/view/WindowInsetsTest.java b/core/tests/coretests/src/android/view/WindowInsetsTest.java index ab4543cb8001..ba1204b9cc23 100644 --- a/core/tests/coretests/src/android/view/WindowInsetsTest.java +++ b/core/tests/coretests/src/android/view/WindowInsetsTest.java @@ -17,6 +17,7 @@ package android.view; import static android.view.WindowInsets.Type.SIZE; +import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.systemBars; import static org.junit.Assert.assertEquals; @@ -26,12 +27,14 @@ import android.graphics.Insets; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; + @RunWith(AndroidJUnit4.class) @SmallTest @Presubmit @@ -68,4 +71,17 @@ public class WindowInsetsTest { true /* compatIgnoreVisibility */, null, null, 0, 0); assertEquals(Insets.of(0, 10, 0, 0), windowInsets.getSystemWindowInsets()); } + + @Test + public void testSetBoundingRectsInBuilder_noInsets_preservedInWindowInsets() { + final List<Rect> rects = List.of(new Rect(0, 0, 50, 100)); + final WindowInsets insets = + new WindowInsets.Builder() + .setBoundingRects(captionBar(), rects) + .setBoundingRectsIgnoringVisibility(captionBar(), rects) + .build(); + + assertEquals(rects, insets.getBoundingRects(captionBar())); + assertEquals(rects, insets.getBoundingRectsIgnoringVisibility(captionBar())); + } } diff --git a/core/tests/coretests/src/android/view/WindowManagerTests.java b/core/tests/coretests/src/android/view/WindowManagerTests.java index c5a9d480771e..211d768dde93 100644 --- a/core/tests/coretests/src/android/view/WindowManagerTests.java +++ b/core/tests/coretests/src/android/view/WindowManagerTests.java @@ -25,8 +25,8 @@ import static org.junit.Assume.assumeTrue; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; diff --git a/core/tests/coretests/src/android/view/WindowMetricsTest.java b/core/tests/coretests/src/android/view/WindowMetricsTest.java index 39ea8af6fed9..f3ddfa68489f 100644 --- a/core/tests/coretests/src/android/view/WindowMetricsTest.java +++ b/core/tests/coretests/src/android/view/WindowMetricsTest.java @@ -27,9 +27,9 @@ import android.hardware.display.DisplayManager; import android.os.Handler; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java index 3d4918b1bd42..2d82d231e279 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java @@ -46,7 +46,7 @@ public class AccessibilityNodeInfoTest { // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest: // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo, // and assertAccessibilityNodeInfoCleared in that class. - private static final int NUM_MARSHALLED_PROPERTIES = 43; + private static final int NUM_MARSHALLED_PROPERTIES = 44; /** * The number of properties that are purposely not marshalled diff --git a/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java b/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java index 3147eac30705..8db13c845b38 100644 --- a/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java +++ b/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java @@ -30,8 +30,8 @@ import static android.window.SystemPerformanceHinter.HINT_SF_FRAME_RATE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -42,14 +42,16 @@ import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl; import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.HashMap; @@ -63,6 +65,9 @@ import java.util.HashMap; @RunWith(AndroidJUnit4.class) public class SystemPerformanceHinterTests { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + private static final int DEFAULT_DISPLAY_ID = android.view.Display.DEFAULT_DISPLAY; private static final int SECONDARY_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1; private static final int NO_ROOT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2; @@ -83,8 +88,6 @@ public class SystemPerformanceHinterTests { @Before public void setUpOnce() { - MockitoAnnotations.initMocks(this); - mDefaultDisplayRoot = new SurfaceControl(); mSecondaryDisplayRoot = new SurfaceControl(); mRootProvider = new SystemPerformanceHinterTests.RootProvider(); diff --git a/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java b/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java index 2dadb20d8f2c..4589607adb82 100644 --- a/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java +++ b/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java @@ -24,9 +24,9 @@ import android.content.Context; import android.platform.test.annotations.Presubmit; import android.view.WindowManager; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/window/WindowContextControllerTest.java b/core/tests/coretests/src/android/window/WindowContextControllerTest.java index 30c0f2b682db..1f60b3121192 100644 --- a/core/tests/coretests/src/android/window/WindowContextControllerTest.java +++ b/core/tests/coretests/src/android/window/WindowContextControllerTest.java @@ -34,14 +34,16 @@ import static org.mockito.Mockito.verify; import android.os.Binder; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Tests for {@link WindowContextController} @@ -56,6 +58,10 @@ import org.mockito.MockitoAnnotations; @SmallTest @Presubmit public class WindowContextControllerTest { + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + private WindowContextController mController; @Mock private WindowTokenClientController mWindowTokenClientController; @@ -64,7 +70,6 @@ public class WindowContextControllerTest { @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); mController = spy(new WindowContextController(mMockToken)); doReturn(mWindowTokenClientController).when(mController).getWindowTokenClientController(); doNothing().when(mMockToken).onConfigurationChanged(any(), anyInt(), anyBoolean()); diff --git a/core/tests/coretests/src/android/window/WindowContextTest.java b/core/tests/coretests/src/android/window/WindowContextTest.java index b2a404449de7..f1fbd559b8f8 100644 --- a/core/tests/coretests/src/android/window/WindowContextTest.java +++ b/core/tests/coretests/src/android/window/WindowContextTest.java @@ -53,10 +53,10 @@ import android.view.WindowManagerGlobal; import android.view.WindowManagerImpl; import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; diff --git a/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java b/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java index 7cbb6b44ea57..accc0206e6e0 100644 --- a/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java +++ b/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java @@ -31,9 +31,9 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.WindowInsets; import android.view.WindowMetrics; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; import com.android.window.flags.Flags; diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index 9ae96a0fc9d8..d153edd9da39 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -44,18 +44,20 @@ import android.view.IWindowSession; import android.view.ImeBackAnimationController; import android.view.MotionEvent; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; @@ -70,6 +72,10 @@ import java.util.List; @SmallTest @Presubmit public class WindowOnBackInvokedDispatcherTest { + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + @Mock private IWindowSession mWindowSession; @Mock @@ -106,8 +112,6 @@ public class WindowOnBackInvokedDispatcherTest { @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - doReturn(true).when(mApplicationInfo).isOnBackInvokedCallbackEnabled(); doReturn(mApplicationInfo).when(mContext).getApplicationInfo(); diff --git a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java index a21c91782214..a3725af04ba6 100644 --- a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java +++ b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java @@ -39,9 +39,11 @@ import android.view.IWindowManager; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** * Tests for {@link WindowTokenClientController}. @@ -53,6 +55,9 @@ import org.mockito.MockitoAnnotations; @Presubmit public class WindowTokenClientControllerTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + @Mock private IWindowManager mWindowManagerService; @Mock @@ -67,7 +72,6 @@ public class WindowTokenClientControllerTest { @Before public void setup() { - MockitoAnnotations.initMocks(this); mController = spy(WindowTokenClientController.createInstanceForTesting()); doReturn(mWindowManagerService).when(mController).getWindowManagerService(); mWindowContextInfo = new WindowContextInfo(mConfiguration, DEFAULT_DISPLAY); diff --git a/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java b/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java index 9292f667b27c..aa4c28a55865 100644 --- a/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java +++ b/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java @@ -20,8 +20,8 @@ import static com.android.window.flags.Flags.taskFragmentSystemOrganizerFlag; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java new file mode 100644 index 000000000000..537dd69d017d --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java @@ -0,0 +1,110 @@ +/* + * 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.internal.policy; + +import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_DEFAULT; +import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_SLEEP_ON_FOLD; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link FoldLockSettingsObserver}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class FoldLockSettingsObserverTest { + @Mock + private Context mContext; + @Mock + private Handler mHandler; + @Mock + private ContentResolver mContentResolver; + + private FoldLockSettingsObserver mFoldLockSettingsObserver; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFoldLockSettingsObserver = + spy(new FoldLockSettingsObserver(mHandler, mContext)); + + doReturn(mContentResolver).when(mContext).getContentResolver(); + doReturn(SETTING_VALUE_DEFAULT).when(mFoldLockSettingsObserver).request(); + + mFoldLockSettingsObserver.register(); + } + + @Test + public void shouldRegister() { + doReturn(mContentResolver).when(mContext).getContentResolver(); + + mFoldLockSettingsObserver.register(); + + verify(mContentResolver).registerContentObserver( + Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR), + false /*notifyForDescendants */, + mFoldLockSettingsObserver, + UserHandle.USER_ALL + ); + } + + @Test + public void shouldUnregister() { + mFoldLockSettingsObserver.unregister(); + + verify(mContentResolver).unregisterContentObserver(mFoldLockSettingsObserver); + } + + @Test + public void shouldCacheNewValue() { + // Reset the mock's behavior and call count to zero. + reset(mFoldLockSettingsObserver); + doReturn(SETTING_VALUE_SLEEP_ON_FOLD).when(mFoldLockSettingsObserver).request(); + + // Setting is DEFAULT at first. + assertEquals(SETTING_VALUE_DEFAULT, mFoldLockSettingsObserver.mFoldLockSetting); + + // Cache new setting. + mFoldLockSettingsObserver.requestAndCacheFoldLockSetting(); + + // Check that setter was called once and change went through properly. + verify(mFoldLockSettingsObserver).setCurrentFoldSetting(anyString()); + assertTrue(mFoldLockSettingsObserver.isSleepOnFold()); + } +} diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index df95a91d72d7..b83931fa0615 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -35,6 +35,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.graphics.fonts.FontVariationAxis; import android.os.Build; import android.os.LocaleList; +import android.text.ClientFlags; import android.text.GraphicsOperations; import android.text.SpannableString; import android.text.SpannedString; @@ -1540,8 +1541,21 @@ public class Paint { * @return typeface */ public Typeface setTypeface(Typeface typeface) { + return setTypefaceInternal(typeface, true); + } + + private Typeface setTypefaceInternal(Typeface typeface, boolean clearFontVariationSettings) { final long typefaceNative = typeface == null ? 0 : typeface.native_instance; nSetTypeface(mNativePaint, typefaceNative); + + if (ClientFlags.clearFontVariationSettings()) { + if (clearFontVariationSettings && !Objects.equals(mTypeface, typeface)) { + // We cannot call setFontVariationSetting with empty string or null because it calls + // setTypeface method. To avoid recursive setTypeface call, manually resetting + // mFontVariationSettings. + mFontVariationSettings = null; + } + } mTypeface = typeface; return typeface; } @@ -2037,6 +2051,14 @@ public class Paint { * </li> * </ul> * + * Note: This method replaces the Typeface previously set to this instance. + * Until API {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, any caller of + * {@link #setTypeface(Typeface)} should call this method with empty settings, then call + * {@link #setTypeface(Typeface)}, then call this method with preferred variation settings. + * The device API more than {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, the + * {@link #setTypeface(Typeface)} method clears font variation settings. So caller of + * {@link #setTypeface(Typeface)} should call this method again for applying variation settings. + * * @param fontVariationSettings font variation settings. You can pass null or empty string as * no variation settings. * @@ -2059,8 +2081,8 @@ public class Paint { if (settings == null || settings.length() == 0) { mFontVariationSettings = null; - setTypeface(Typeface.createFromTypefaceWithVariation(mTypeface, - Collections.emptyList())); + setTypefaceInternal(Typeface.createFromTypefaceWithVariation(mTypeface, + Collections.emptyList()), false); return true; } @@ -2078,7 +2100,8 @@ public class Paint { return false; } mFontVariationSettings = settings; - setTypeface(Typeface.createFromTypefaceWithVariation(targetTypeface, filteredAxes)); + setTypefaceInternal(Typeface.createFromTypefaceWithVariation(targetTypeface, filteredAxes), + false); return true; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index ecf47209a802..7f11feaa585e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -39,6 +39,8 @@ import androidx.window.extensions.embedding.SplitController; import androidx.window.extensions.layout.WindowLayoutComponent; import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import com.android.window.flags.Flags; + import java.util.Objects; @@ -55,11 +57,9 @@ class WindowExtensionsImpl implements WindowExtensions { */ private static final int NO_LEVEL_OVERRIDE = -1; - /** - * The min version of the WM Extensions that must be supported in the current platform version. - */ - @VisibleForTesting - static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6; + private static final int EXTENSIONS_VERSION_V7 = 7; + + private static final int EXTENSIONS_VERSION_V6 = 6; private final Object mLock = new Object(); private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; @@ -67,7 +67,6 @@ class WindowExtensionsImpl implements WindowExtensions { private volatile SplitController mSplitController; private volatile WindowAreaComponent mWindowAreaComponent; - private final int mVersion = EXTENSIONS_VERSION_CURRENT_PLATFORM; private final boolean mIsActivityEmbeddingEnabled; WindowExtensionsImpl() { @@ -76,9 +75,22 @@ class WindowExtensionsImpl implements WindowExtensions { Log.i(TAG, generateLogMessage()); } + /** + * The min version of the WM Extensions that must be supported in the current platform version. + */ + @VisibleForTesting + static int getExtensionsVersionCurrentPlatform() { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + // Activity Embedding animation customization is the only major feature for v7. + return EXTENSIONS_VERSION_V7; + } else { + return EXTENSIONS_VERSION_V6; + } + } + private String generateLogMessage() { final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, " - + "vendor API level=" + mVersion); + + "vendor API level=" + getExtensionsVersionCurrentPlatform()); final int levelOverride = getLevelOverride(); if (levelOverride != NO_LEVEL_OVERRIDE) { logBuilder.append(", override to ").append(levelOverride); @@ -91,7 +103,12 @@ class WindowExtensionsImpl implements WindowExtensions { @Override public int getVendorApiLevel() { final int levelOverride = getLevelOverride(); - return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion; + return hasLevelOverride() ? levelOverride : getExtensionsVersionCurrentPlatform(); + } + + @VisibleForTesting + boolean hasLevelOverride() { + return getLevelOverride() != NO_LEVEL_OVERRIDE; } private int getLevelOverride() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index ea60b1531a3f..f1e7ef5ce123 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -1359,4 +1359,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration, windowLayoutInfo); } + + @VisibleForTesting + @NonNull + static String positionToString(@ContainerPosition int position) { + return switch (position) { + case CONTAINER_POSITION_LEFT -> "left"; + case CONTAINER_POSITION_TOP -> "top"; + case CONTAINER_POSITION_RIGHT -> "right"; + case CONTAINER_POSITION_BOTTOM -> "bottom"; + default -> "Unknown position:" + position; + }; + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp index 61ea51a35f58..139ddda5af3c 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -32,6 +32,7 @@ android_test { ], static_libs: [ + "TestParameterInjector", "androidx.window.extensions", "androidx.window.extensions.core_core", "junit", diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index c5aaddc4e0ed..92f48141b607 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -16,7 +16,7 @@ package androidx.window.extensions; -import static androidx.window.extensions.WindowExtensionsImpl.EXTENSIONS_VERSION_CURRENT_PLATFORM; +import static androidx.window.extensions.WindowExtensionsImpl.getExtensionsVersionCurrentPlatform; import static com.google.common.truth.Truth.assertThat; @@ -59,7 +59,8 @@ public class WindowExtensionsTest { @Test public void testGetVendorApiLevel_extensionsEnabled_matchesCurrentVersion() { assumeTrue(WindowManager.hasWindowExtensionsEnabled()); - assertThat(mVersion).isEqualTo(EXTENSIONS_VERSION_CURRENT_PLATFORM); + assumeFalse(((WindowExtensionsImpl) mExtensions).hasLevelOverride()); + assertThat(mVersion).isEqualTo(getExtensionsVersionCurrentPlatform()); } @Test diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 325750243744..1c4c8870b26f 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -35,6 +35,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; import static androidx.window.extensions.embedding.SplitPresenter.getOverlayPosition; +import static androidx.window.extensions.embedding.SplitPresenter.positionToString; import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; @@ -78,7 +79,6 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.extensions.layout.WindowLayoutComponentImpl; @@ -86,6 +86,9 @@ import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.window.flags.Flags; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -108,7 +111,7 @@ import java.util.List; @SuppressWarnings("GuardedBy") @Presubmit @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(TestParameterInjector.class) public class OverlayPresentationTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); @@ -875,57 +878,70 @@ public class OverlayPresentationTest { eq(overlayContainer.getTaskFragmentToken()), eq(activityToken)); } - // TODO(b/243518738): Rewrite with TestParameter. - @Test - public void testGetOverlayPosition() { - assertWithMessage("It must be position left for left overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.left, - TASK_BOUNDS.top, - TASK_BOUNDS.right / 2, - TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_LEFT); - assertWithMessage("It must be position left for shrunk left overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.left, - TASK_BOUNDS.top + 20, - TASK_BOUNDS.right / 2, - TASK_BOUNDS.bottom - 20), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_LEFT); - assertWithMessage("It must be position left for top overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.left, - TASK_BOUNDS.top, - TASK_BOUNDS.right, - TASK_BOUNDS.bottom / 2), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_TOP); - assertWithMessage("It must be position left for shrunk top overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.left + 20, - TASK_BOUNDS.top, - TASK_BOUNDS.right - 20, - TASK_BOUNDS.bottom / 2), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_TOP); - assertWithMessage("It must be position left for right overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.right / 2, - TASK_BOUNDS.top, - TASK_BOUNDS.right, - TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_RIGHT); - assertWithMessage("It must be position left for shrunk right overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.right / 2, - TASK_BOUNDS.top + 20, - TASK_BOUNDS.right, - TASK_BOUNDS.bottom - 20), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_RIGHT); - assertWithMessage("It must be position left for bottom overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.left, - TASK_BOUNDS.bottom / 2, - TASK_BOUNDS.right, - TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_BOTTOM); - assertWithMessage("It must be position left for shrunk bottom overlay.") - .that(getOverlayPosition(new Rect( - TASK_BOUNDS.left + 20, - TASK_BOUNDS.bottom / 20, - TASK_BOUNDS.right - 20, - TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_BOTTOM); + @Test + public void testGetOverlayPosition(@TestParameter OverlayPositionTestParams params) { + final Rect taskBounds = new Rect(TASK_BOUNDS); + final Rect overlayBounds = params.toOverlayBounds(); + final int overlayPosition = getOverlayPosition(overlayBounds, taskBounds); + + assertWithMessage("The overlay position must be " + + positionToString(params.mPosition) + ", but is " + + positionToString(overlayPosition) + + ", parent bounds=" + taskBounds + ", overlay bounds=" + overlayBounds) + .that(overlayPosition).isEqualTo(params.mPosition); + } + + private enum OverlayPositionTestParams { + LEFT_OVERLAY(CONTAINER_POSITION_LEFT, false /* shouldBeShrunk */), + LEFT_SHRUNK_OVERLAY(CONTAINER_POSITION_LEFT, true /* shouldBeShrunk */), + TOP_OVERLAY(CONTAINER_POSITION_TOP, false /* shouldBeShrunk */), + TOP_SHRUNK_OVERLAY(CONTAINER_POSITION_TOP, true /* shouldBeShrunk */), + RIGHT_OVERLAY(CONTAINER_POSITION_RIGHT, false /* shouldBeShrunk */), + RIGHT_SHRUNK_OVERLAY(CONTAINER_POSITION_RIGHT, true /* shouldBeShrunk */), + BOTTOM_OVERLAY(CONTAINER_POSITION_BOTTOM, false /* shouldBeShrunk */), + BOTTOM_SHRUNK_OVERLAY(CONTAINER_POSITION_BOTTOM, true /* shouldBeShrunk */); + + @SplitPresenter.ContainerPosition + private final int mPosition; + + private final boolean mShouldBeShrunk; + + OverlayPositionTestParams( + @SplitPresenter.ContainerPosition int position, boolean shouldBeShrunk) { + mPosition = position; + mShouldBeShrunk = shouldBeShrunk; + } + + @NonNull + private Rect toOverlayBounds() { + Rect r = new Rect(TASK_BOUNDS); + final int offset = mShouldBeShrunk ? 20 : 0; + switch (mPosition) { + case CONTAINER_POSITION_LEFT: + r.top += offset; + r.right /= 2; + r.bottom -= offset; + break; + case CONTAINER_POSITION_TOP: + r.left += offset; + r.right -= offset; + r.bottom /= 2; + break; + case CONTAINER_POSITION_RIGHT: + r.left = r.right / 2; + r.top += offset; + r.bottom -= offset; + break; + case CONTAINER_POSITION_BOTTOM: + r.left += offset; + r.right -= offset; + r.top = r.bottom / 2; + break; + default: + throw new IllegalArgumentException("Invalid position: " + mPosition); + } + return r; + } } /** diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index e6cb3a08119a..5135e9ee14bc 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -239,6 +239,9 @@ android_library { "wmshell.protolog.json.gz", "wmshell.protolog.pb", ], + flags_packages: [ + "com_android_wm_shell_flags", + ], kotlincflags: ["-Xjvm-default=all"], manifest: "AndroidManifest.xml", plugins: ["dagger2-compiler"], diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 52ae93f5ebf1..bbbc23e8b922 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -34,6 +34,7 @@ <activity android:name=".bubbles.shortcut.CreateBubbleShortcutActivity" + android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles" android:exported="true" android:excludeFromRecents="true" android:theme="@android:style/Theme.NoDisplay" @@ -47,6 +48,7 @@ <activity android:name=".bubbles.shortcut.ShowBubblesActivity" + android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles" android:exported="true" android:excludeFromRecents="true" android:theme="@android:style/Theme.NoDisplay" > diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt index f0d80a02243a..d3fc49bcb766 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt @@ -39,8 +39,6 @@ enum class DesktopModeFlags( /** * Determines state of flag based on the actual flag and desktop mode developer option overrides. - * - * Note, this method makes sure that a constant developer toggle overrides is read until reboot. */ fun isEnabled(context: Context): Boolean = if (!Flags.showDesktopWindowingDevOption() || @@ -65,7 +63,7 @@ enum class DesktopModeFlags( ?: run { val override = getToggleOverrideFromSystem(context) // Cache toggle override the first time we encounter context. Override does not change - // with context, as context is just used to fetch System Property and Settings.Global + // with context, as context is just used to fetch Settings.Global cachedToggleOverride = override Log.d(TAG, "Toggle override initialized to: $override") override @@ -74,29 +72,13 @@ enum class DesktopModeFlags( return override } - private fun getToggleOverrideFromSystem(context: Context): ToggleOverride { - // A non-persistent System Property is used to store override to ensure it remains - // constant till reboot. - val overrideFromSystemProperties: ToggleOverride? = - System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null).convertToToggleOverride() - return overrideFromSystemProperties - ?: run { - // Read Setting Global if System Property is not present (just after reboot) - // or not valid (user manually changed the value) - val overrideFromSettingsGlobal = - convertToToggleOverrideWithFallback( - Settings.Global.getInt( - context.contentResolver, - Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, - ToggleOverride.OVERRIDE_UNSET.setting), - ToggleOverride.OVERRIDE_UNSET) - // Initialize System Property - System.setProperty( - SYSTEM_PROPERTY_OVERRIDE_KEY, overrideFromSettingsGlobal.setting.toString()) - - overrideFromSettingsGlobal - } - } + private fun getToggleOverrideFromSystem(context: Context): ToggleOverride = + convertToToggleOverrideWithFallback( + Settings.Global.getInt( + context.contentResolver, + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, + ToggleOverride.OVERRIDE_UNSET.setting), + ToggleOverride.OVERRIDE_UNSET) /** * Override state of desktop mode developer option toggle. @@ -113,27 +95,12 @@ enum class DesktopModeFlags( OVERRIDE_ON(1) } - private fun String?.convertToToggleOverride(): ToggleOverride? { - val intValue = this?.toIntOrNull() ?: return null - return settingToToggleOverrideMap[intValue] - ?: run { - Log.w(TAG, "Unknown toggleOverride int $intValue") - null - } - } - companion object { private const val TAG = "DesktopModeFlags" /** - * Key for non-persistent System Property which is used to store desktop windowing developer - * option overrides. - */ - private const val SYSTEM_PROPERTY_OVERRIDE_KEY = "sys.wmshell.desktopmode.dev_toggle_override" - - /** * Local cache for toggle override, which is initialized once on its first access. It needs to - * be refreshed only on reboots as overridden state takes effect on reboots. + * be refreshed only on reboots as overridden state is expected to take effect on reboots. */ private var cachedToggleOverride: ToggleOverride? = null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index f7a5c271a729..d4d9d003bc0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -16,7 +16,7 @@ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -225,8 +225,7 @@ public class BubbleExpandedView extends LinearLayout { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index c79d9c4942bf..5e2141aa639e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -15,7 +15,7 @@ */ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -103,8 +103,7 @@ public class BubbleTaskViewHelper { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index 64a1b0c804da..140d7765e5c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -30,6 +30,7 @@ import android.graphics.Rect; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; +import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; @@ -42,9 +43,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -69,26 +68,36 @@ public class PipBoundsState { @Retention(RetentionPolicy.SOURCE) public @interface StashType {} + public static final int NAMED_KCA_LAUNCHER_SHELF = 0; + public static final int NAMED_KCA_TABLETOP_MODE = 1; + + @IntDef(prefix = { "NAMED_KCA_" }, value = { + NAMED_KCA_LAUNCHER_SHELF, + NAMED_KCA_TABLETOP_MODE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NamedKca {} + private static final String TAG = PipBoundsState.class.getSimpleName(); - private final @NonNull Rect mBounds = new Rect(); - private final @NonNull Rect mMovementBounds = new Rect(); - private final @NonNull Rect mNormalBounds = new Rect(); - private final @NonNull Rect mExpandedBounds = new Rect(); - private final @NonNull Rect mNormalMovementBounds = new Rect(); - private final @NonNull Rect mExpandedMovementBounds = new Rect(); - private final @NonNull PipDisplayLayoutState mPipDisplayLayoutState; + @NonNull private final Rect mBounds = new Rect(); + @NonNull private final Rect mMovementBounds = new Rect(); + @NonNull private final Rect mNormalBounds = new Rect(); + @NonNull private final Rect mExpandedBounds = new Rect(); + @NonNull private final Rect mNormalMovementBounds = new Rect(); + @NonNull private final Rect mExpandedMovementBounds = new Rect(); + @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState; private final Point mMaxSize = new Point(); private final Point mMinSize = new Point(); - private final @NonNull Context mContext; + @NonNull private final Context mContext; private float mAspectRatio; private int mStashedState = STASH_TYPE_NONE; private int mStashOffset; - private @Nullable PipReentryState mPipReentryState; + @Nullable private PipReentryState mPipReentryState; private final LauncherState mLauncherState = new LauncherState(); - private final @NonNull SizeSpecSource mSizeSpecSource; - private @Nullable ComponentName mLastPipComponentName; - private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState(); + @NonNull private final SizeSpecSource mSizeSpecSource; + @Nullable private ComponentName mLastPipComponentName; + @NonNull private final MotionBoundsState mMotionBoundsState = new MotionBoundsState(); private boolean mIsImeShowing; private int mImeHeight; private boolean mIsShelfShowing; @@ -120,12 +129,18 @@ public class PipBoundsState { * as unrestricted keep clear area. Values in this map would be appended to * {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only. */ - private final Map<String, Rect> mNamedUnrestrictedKeepClearAreas = new HashMap<>(); + private final SparseArray<Rect> mNamedUnrestrictedKeepClearAreas = new SparseArray<>(); - private @Nullable Runnable mOnMinimalSizeChangeCallback; - private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; - private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); - private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); + @Nullable private Runnable mOnMinimalSizeChangeCallback; + @Nullable private TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; + private final List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); + private final List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); + + /** + * This is used to set the launcher shelf height ahead of non-auto-enter-pip animation, + * to avoid the race condition. See also {@link #NAMED_KCA_LAUNCHER_SHELF}. + */ + public final Rect mCachedLauncherShelfHeightKeepClearArea = new Rect(); // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -430,17 +445,32 @@ public class PipBoundsState { mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas); } - /** Add a named unrestricted keep clear area. */ - public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) { - mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea); + /** Set a named unrestricted keep clear area. */ + public void setNamedUnrestrictedKeepClearArea( + @NamedKca int tag, @Nullable Rect unrestrictedArea) { + if (unrestrictedArea == null) { + mNamedUnrestrictedKeepClearAreas.remove(tag); + } else { + mNamedUnrestrictedKeepClearAreas.put(tag, unrestrictedArea); + if (tag == NAMED_KCA_LAUNCHER_SHELF) { + mCachedLauncherShelfHeightKeepClearArea.set(unrestrictedArea); + } + } } - /** Remove a named unrestricted keep clear area. */ - public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) { - mNamedUnrestrictedKeepClearAreas.remove(name); + /** + * Forcefully set the keep-clear-area for launcher shelf height if applicable. + * This is used for entering PiP in button navigation mode to make sure the destination bounds + * calculation includes the shelf height, to avoid race conditions that such callback is sent + * from Launcher after the entering animation is started. + */ + public void mayUseCachedLauncherShelfHeight() { + if (!mCachedLauncherShelfHeightKeepClearArea.isEmpty()) { + setNamedUnrestrictedKeepClearArea( + NAMED_KCA_LAUNCHER_SHELF, mCachedLauncherShelfHeightKeepClearArea); + } } - /** * @return restricted keep clear areas. */ @@ -454,9 +484,12 @@ public class PipBoundsState { */ @NonNull public Set<Rect> getUnrestrictedKeepClearAreas() { - if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas; + if (mNamedUnrestrictedKeepClearAreas.size() == 0) return mUnrestrictedKeepClearAreas; final Set<Rect> unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas); - unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values()); + for (int i = 0; i < mNamedUnrestrictedKeepClearAreas.size(); i++) { + final int key = mNamedUnrestrictedKeepClearAreas.keyAt(i); + unrestrictedAreas.add(mNamedUnrestrictedKeepClearAreas.get(key)); + } return unrestrictedAreas; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index e713af6a5311..700742acfb43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -211,6 +211,7 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellMainThread Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, IWindowManager windowManager, ShellCommandHandler shellCommandHandler, @@ -229,6 +230,7 @@ public abstract class WMShellModule { mainExecutor, mainHandler, mainChoreographer, + bgExecutor, shellInit, shellCommandHandler, windowManager, @@ -246,6 +248,7 @@ public abstract class WMShellModule { context, mainHandler, mainExecutor, + bgExecutor, mainChoreographer, windowManager, shellInit, @@ -366,13 +369,14 @@ public abstract class WMShellModule { Optional<WindowDecorViewModel> windowDecorViewModel, Optional<DesktopTasksController> desktopTasksController, MultiInstanceHelper multiInstanceHelper, - @ShellMainThread ShellExecutor mainExecutor) { + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, windowDecorViewModel, desktopTasksController, null /* stageCoordinator */, - multiInstanceHelper, mainExecutor); + multiInstanceHelper, mainExecutor, mainHandler); } // @@ -530,7 +534,8 @@ public abstract class WMShellModule { MultiInstanceHelper multiInstanceHelper, @ShellMainThread ShellExecutor mainExecutor, Optional<DesktopTasksLimiter> desktopTasksLimiter, - Optional<RecentTasksController> recentTasksController) { + Optional<RecentTasksController> recentTasksController, + InteractionJankMonitor interactionJankMonitor) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, keyguardManager, enterDesktopTransitionHandler, @@ -538,7 +543,8 @@ public abstract class WMShellModule { dragToDesktopTransitionHandler, desktopModeTaskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, - mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null)); + mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null), + interactionJankMonitor); } @WMSingleton @@ -564,9 +570,10 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - Optional<DesktopTasksLimiter> desktopTasksLimiter) { + Optional<DesktopTasksLimiter> desktopTasksLimiter, + InteractionJankMonitor interactionJankMonitor) { return new DragToDesktopTransitionHandler(context, transitions, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, interactionJankMonitor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 1a9c304e2aee..037fbb235bd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -206,12 +206,13 @@ public abstract class Pip1Module { @WMSingleton @Provides static PipMotionHelper providePipMotionHelper(Context context, + @ShellMainThread ShellExecutor mainExecutor, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, Optional<PipPerfHintController> pipPerfHintControllerOptional) { - return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer, + return new PipMotionHelper(context, mainExecutor, pipBoundsState, pipTaskOrganizer, menuController, pipSnapAlgorithm, pipTransitionController, floatingContentCoordinator, pipPerfHintControllerOptional); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index 066b5ad39d0f..73aa7ceea68d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -347,7 +347,7 @@ class DesktopModeLoggerTransitionObserver( else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, - "Unknown enter reason for transition type ${transitionInfo.type}", + "Unknown enter reason for transition type: %s", transitionInfo.type ) EnterReason.UNKNOWN_ENTER @@ -368,7 +368,7 @@ class DesktopModeLoggerTransitionObserver( else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, - "Unknown exit reason for transition type ${transitionInfo.type}", + "Unknown exit reason for transition type: %s", transitionInfo.type ) ExitReason.UNKNOWN_EXIT diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 1bf125938e6f..da212e704b24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -66,7 +66,7 @@ fun calculateInitialBounds( idealSize } } else { - maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio) + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } ORIENTATION_PORTRAIT -> { @@ -85,13 +85,13 @@ fun calculateInitialBounds( } else { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { // Apply custom app width and calculate maximum size - maximumSizeMaintainingAspectRatio( + maximizeSizeGivenAspectRatio( taskInfo, Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio ) } else { - maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio) + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } } @@ -107,7 +107,7 @@ fun calculateInitialBounds( * Calculates the largest size that can fit in a given area while maintaining a specific aspect * ratio. */ -fun maximumSizeMaintainingAspectRatio( +fun maximizeSizeGivenAspectRatio( taskInfo: RunningTaskInfo, targetArea: Size, aspectRatio: Float diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 9e6099f2e4cc..9fd2c274df2d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -49,6 +49,9 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.BinderThread import com.android.internal.annotations.VisibleForTesting +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags @@ -69,6 +72,7 @@ import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -122,7 +126,8 @@ class DesktopTasksController( private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor, private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, - private val recentTasksController: RecentTasksController? + private val recentTasksController: RecentTasksController?, + private val interactionJankMonitor: InteractionJankMonitor ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -377,12 +382,15 @@ class DesktopTasksController( fun startDragToDesktop( taskInfo: RunningTaskInfo, dragToDesktopValueAnimator: MoveToDesktopAnimator, + taskSurface: SurfaceControl, ) { ProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: startDragToDesktop taskId=%d", taskInfo.taskId ) + interactionJankMonitor.begin(taskSurface, context, + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) dragToDesktopTransitionHandler.startDragToDesktopTransition( taskInfo.taskId, dragToDesktopValueAnimator @@ -671,7 +679,7 @@ class DesktopTasksController( } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) - val newSize = maximumSizeMaintainingAspectRatio(taskInfo, + val newSize = maximizeSizeGivenAspectRatio(taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) val newBounds = centerInArea( newSize, stableBounds, stableBounds.left, stableBounds.top) @@ -818,9 +826,8 @@ class DesktopTasksController( val intent = Intent(context, DesktopWallpaperActivity::class.java) val options = ActivityOptions.makeBasic().apply { - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS } val pendingIntent = PendingIntent.getActivity( @@ -1080,7 +1087,6 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode val targetWindowingMode = @@ -1090,9 +1096,6 @@ class DesktopTasksController( } else { WINDOWING_MODE_FREEFORM } - if (Flags.enableWindowingDynamicInitialBounds()) { - wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo)) - } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.reorder(taskInfo.token, true /* onTop */) if (useDesktopOverrideDensity()) { @@ -1339,33 +1342,42 @@ class DesktopTasksController( * * @param taskInfo the task being dragged. * @param y height of drag, to be checked against status bar height. + * @return the [IndicatorType] used for the resulting transition */ fun onDragPositioningEndThroughStatusBar( inputCoordinates: PointF, taskInfo: RunningTaskInfo, - ) { - val indicator = getVisualIndicator() ?: return + taskSurface: SurfaceControl, + ): IndicatorType { + // End the drag_hold CUJ interaction. + interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) + val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode) when (indicatorType) { - DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { - val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + IndicatorType.TO_DESKTOP_INDICATOR -> { + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) + ?: return IndicatorType.NO_INDICATOR + // Start a new jank interaction for the drag release to desktop window animation. + interactionJankMonitor.begin(taskSurface, context, + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop") if (Flags.enableWindowingDynamicInitialBounds()) { finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo)) } else { finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout)) } } - DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, - DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { + IndicatorType.NO_INDICATOR, + IndicatorType.TO_FULLSCREEN_INDICATOR -> { cancelDragToDesktop(taskInfo) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { requestSplit(taskInfo, leftOrTop = true) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { requestSplit(taskInfo, leftOrTop = false) } } + return indicatorType } /** Update the exclusion region for a specified task */ @@ -1426,7 +1438,6 @@ class DesktopTasksController( setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED ) - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index ddee8fac8f44..9e79eddb0e59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -30,6 +30,9 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import com.android.internal.protolog.ProtoLog +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE +import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT @@ -57,17 +60,20 @@ class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val transactionSupplier: Supplier<SurfaceControl.Transaction> + private val interactionJankMonitor: InteractionJankMonitor, + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { constructor( context: Context, transitions: Transitions, - rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + interactionJankMonitor: InteractionJankMonitor ) : this( context, transitions, rootTaskDisplayAreaOrganizer, + interactionJankMonitor, Supplier { SurfaceControl.Transaction() } ) @@ -567,6 +573,8 @@ class DragToDesktopTransitionHandler( onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) startTransitionFinishCb.onTransitionFinished(null /* null */) clearState() + interactionJankMonitor.end( + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) } } ) @@ -604,6 +612,10 @@ class DragToDesktopTransitionHandler( "DragToDesktop: onTransitionConsumed() start transition aborted" ) state.startAborted = true + // Cancel CUJ interaction if the transition is aborted. + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) + } else if (state.cancelTransitionToken != transition) { + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 95fe8b6f1f4e..7e0362475f21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,7 +16,7 @@ package com.android.wm.shell.draganddrop; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -280,8 +280,7 @@ public class DragAndDropPolicy { baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); // Put BAL flags to avoid activity start aborted. baseActivityOpts.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final Bundle opts = baseActivityOpts.toBundle(); if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index a749019046f8..b27c428f1693 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -16,10 +16,12 @@ package com.android.wm.shell.pip; +import android.annotation.NonNull; import android.graphics.Rect; import com.android.wm.shell.shared.annotations.ExternalThread; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -69,9 +71,10 @@ public interface Pip { default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { } /** - * @return {@link PipTransitionController} instance. + * Register {@link PipTransitionController.PipTransitionCallback} to listen on PiP transition + * started / finished callbacks. */ - default PipTransitionController getPipTransitionController() { - return null; - } + default void registerPipTransitionCallback( + @NonNull PipTransitionController.PipTransitionCallback callback, + @NonNull Executor executor) { } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index a8346a9b3b48..852382ddfba1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -230,6 +230,7 @@ public class PipAnimationController { /** * Quietly cancel the animator by removing the listeners first. + * TODO(b/275003573): deprecate this, cancelling without the proper callbacks is problematic. */ static void quietCancel(@NonNull ValueAnimator animator) { animator.removeAllUpdateListeners(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 8d63ff2a3a5d..723a53128bd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -423,7 +423,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, }); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); - pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback( + mPipTransitionCallback, mMainExecutor); } } @@ -495,7 +496,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "startSwipePipToHome: %s, state=%s", componentName, mPipTransitionState); mPipTransitionState.setInSwipePipToHomeTransition(true); - sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + if (!ENABLE_SHELL_TRANSITIONS) { + sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + } setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); return mPipBoundsAlgorithm.getEntryDestinationBounds(); } @@ -2023,7 +2026,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, removeContentOverlay(mPipOverlay, null /* callback */); } if (animator != null) { - PipAnimationController.quietCancel(animator); + animator.cancel(); mPipAnimationController.resetAnimatorState(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index e5633de2a3a2..a52141c5f9d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -1020,6 +1020,9 @@ public class PipTransition extends PipTransitionController { mPipMenuController.attach(leash); } + // Make sure we have the launcher shelf into destination bounds calculation + // before the animator starts. + mPipBoundsState.mayUseCachedLauncherShelfHeight(); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = pipChange.getStartAbsBounds(); @@ -1173,7 +1176,13 @@ public class PipTransition extends PipTransitionController { .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE); } - final Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds(); + sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + // Both Shell and Launcher calculate their own "adjusted" source-rect-hint values based on + // appBounds being source bounds when entering PiP. + final Rect sourceBounds = swipePipToHomeOverlay == null + ? pipTaskInfo.configuration.windowConfiguration.getBounds() + : mPipOrganizer.mAppBounds; + final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index b1dd4f1c1395..fc9e2becf98c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -53,8 +53,9 @@ import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; /** * Responsible supplying PiP Transitions. @@ -66,7 +67,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + private final Map<PipTransitionCallback, Executor> mPipTransitionCallbacks = new HashMap<>(); protected PipTaskOrganizer mPipOrganizer; protected DefaultMixedHandler mMixedHandler; @@ -183,16 +184,20 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ - public void registerPipTransitionCallback(PipTransitionCallback callback) { - mPipTransitionCallbacks.add(callback); + public void registerPipTransitionCallback( + @NonNull PipTransitionCallback callback, @NonNull Executor executor) { + mPipTransitionCallbacks.put(callback, executor); } protected void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { final Rect pipBounds = mPipBoundsState.getBounds(); - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionStarted(direction, pipBounds); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "sendOnPipTransitionStarted direction=%d, bounds=%s", direction, pipBounds); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionStarted(direction, pipBounds)); } if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) { try { @@ -209,9 +214,12 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionFinished(direction); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "sendOnPipTransitionFinished direction=%d", direction); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionFinished(direction)); } if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) { try { @@ -228,9 +236,12 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void sendOnPipTransitionCancelled( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionCanceled(direction); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "sendOnPipTransitionCancelled direction=%d", direction); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionCanceled(direction)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 26b7e58bc602..7451d2251588 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -106,6 +106,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -478,7 +479,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mShellCommandHandler.addDumpCallback(this::dump, this); mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), INPUT_CONSUMER_PIP, mMainExecutor); - mPipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor); mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { mPipDisplayLayoutState.setDisplayId(displayId); onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -645,9 +646,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb }); mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> { - final String tag = "tabletop-mode"; if (!isInTabletopMode) { - mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_TABLETOP_MODE, null); return; } @@ -656,14 +657,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb if (mTabletopModeController.getPreferredHalfInTabletopMode() == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) { // Prefer top, avoid the bottom half of the display. - mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( - displayBounds.left, displayBounds.centerY(), - displayBounds.right, displayBounds.bottom)); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_TABLETOP_MODE, new Rect( + displayBounds.left, displayBounds.centerY(), + displayBounds.right, displayBounds.bottom)); } else { // Prefer bottom, avoid the top half of the display. - mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( - displayBounds.left, displayBounds.top, - displayBounds.right, displayBounds.centerY())); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_TABLETOP_MODE, new Rect( + displayBounds.left, displayBounds.top, + displayBounds.right, displayBounds.centerY())); } // Try to move the PiP window if we have entered PiP mode. @@ -915,10 +918,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb 0, mPipBoundsState.getDisplayBounds().bottom - height, mPipBoundsState.getDisplayBounds().right, mPipBoundsState.getDisplayBounds().bottom); - mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, rect); updatePipPositionForKeepClearAreas(); } else { - mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, null); // postpone moving in response to hide of Launcher in case there's another change mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); mMainExecutor.executeDelayed( @@ -967,8 +972,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb int launcherRotation, Rect hotseatKeepClearArea) { // preemptively add the keep clear area for Hotseat, so that it is taken into account // when calculating the entry destination bounds of PiP window - mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, - hotseatKeepClearArea); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, hotseatKeepClearArea); onDisplayRotationChangedNotInPip(mContext, launcherRotation); // cache current min/max size Point minSize = mPipBoundsState.getMinSize(); @@ -1220,8 +1225,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public PipTransitionController getPipTransitionController() { - return mPipTransitionController; + public void registerPipTransitionCallback( + PipTransitionController.PipTransitionCallback callback, + Executor executor) { + mMainExecutor.execute(() -> mPipTransitionController.registerPipTransitionCallback( + callback, executor)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index e8d6576bab3b..df3803d54d9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -38,6 +38,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsState; @@ -47,6 +48,7 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.annotations.ShellMainThread; import kotlin.Unit; import kotlin.jvm.functions.Function0; @@ -171,7 +173,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, public void onPipTransitionCanceled(int direction) {} }; - public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, + public PipMotionHelper(Context context, + @ShellMainThread ShellExecutor mainExecutor, + @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, @@ -183,7 +187,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mSnapAlgorithm = snapAlgorithm; mFloatingContentCoordinator = floatingContentCoordinator; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); - pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback, mainExecutor); mResizePipUpdateListener = (target, values) -> { if (mPipBoundsState.getMotionBoundsState().isInMotion()) { mPipTaskOrganizer.scheduleUserResizePip(getBounds(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 62c0944f230b..0ed5079b7fba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -257,7 +257,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private void onInit() { - mPipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor); reloadResources(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java new file mode 100644 index 000000000000..8a9302bcfc98 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.animation; + +import android.animation.Animator; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.content.Context; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Animator that handles bounds animations for entering / exiting PIP. + */ +public class PipEnterExitAnimator extends ValueAnimator + implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { + @IntDef(prefix = {"BOUNDS_"}, value = { + BOUNDS_ENTER, + BOUNDS_EXIT + }) + + @Retention(RetentionPolicy.SOURCE) + public @interface BOUNDS {} + + public static final int BOUNDS_ENTER = 0; + public static final int BOUNDS_EXIT = 1; + + @NonNull private final SurfaceControl mLeash; + private final SurfaceControl.Transaction mStartTransaction; + private final int mEnterAnimationDuration; + private final @BOUNDS int mDirection; + + // optional callbacks for tracking animation start and end + @Nullable private Runnable mAnimationStartCallback; + @Nullable private Runnable mAnimationEndCallback; + + private final Rect mBaseBounds = new Rect(); + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + + // Bounds updated by the evaluator as animator is running. + private final Rect mAnimatedRect = new Rect(); + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private final RectEvaluator mRectEvaluator; + private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; + + public PipEnterExitAnimator(Context context, + @NonNull SurfaceControl leash, + SurfaceControl.Transaction startTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + @BOUNDS int direction) { + mLeash = leash; + mStartTransaction = startTransaction; + mBaseBounds.set(baseBounds); + mStartBounds.set(startBounds); + mAnimatedRect.set(startBounds); + mEndBounds.set(endBounds); + mRectEvaluator = new RectEvaluator(mAnimatedRect); + mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(context); + mDirection = direction; + + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + mEnterAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipEnterAnimationDuration); + + setDuration(mEnterAnimationDuration); + setEvaluator(mRectEvaluator); + addListener(this); + addUpdateListener(this); + } + + public void setAnimationStartCallback(@NonNull Runnable runnable) { + mAnimationStartCallback = runnable; + } + + public void setAnimationEndCallback(@NonNull Runnable runnable) { + mAnimationEndCallback = runnable; + } + + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + // TODO (b/350801661): implement fixed rotation + + mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, null, + mBaseBounds, mAnimatedRect, null, isInPipDirection(), fraction) + .round(tx, mLeash, isInPipDirection()) + .shadow(tx, mLeash, isInPipDirection()); + tx.apply(); + } + + private boolean isInPipDirection() { + return mDirection == BOUNDS_ENTER; + } + + // no-ops + + @Override + public void onAnimationCancel(@NonNull Animator animation) {} + + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 683d30d59b33..33703ad95a9c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -53,6 +53,7 @@ import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipContentOverlay; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; +import com.android.wm.shell.pip2.animation.PipEnterExitAnimator; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -378,12 +379,34 @@ public class PipTransition extends PipTransitionController implements if (pipChange == null) { return false; } - // cache the PiP task token and leash + WindowContainerToken pipTaskToken = pipChange.getContainer(); + if (pipTaskToken == null) { + return false; + } - startTransaction.apply(); - // TODO: b/275910498 Use a new implementation of the PiP animator here. - finishCallback.onTransitionFinished(null); + WindowContainerTransaction finishWct = new WindowContainerTransaction(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + + Rect startBounds = pipChange.getStartAbsBounds(); + Rect endBounds = pipChange.getEndAbsBounds(); + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); + + PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash, + startTransaction, startBounds, startBounds, endBounds, + PipEnterExitAnimator.BOUNDS_ENTER); + + tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), + this::onClientDrawAtTransitionEnd); + finishWct.setBoundsChangeTransaction(pipTaskToken, tx); + + animator.setAnimationEndCallback(() -> { + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct); + }); + + animator.start(); return true; } @@ -421,10 +444,25 @@ public class PipTransition extends PipTransitionController implements @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - startTransaction.apply(); - // TODO: b/275910498 Use a new implementation of the PiP animator here. - finishCallback.onTransitionFinished(null); - mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + TransitionInfo.Change pipChange = getPipChange(info); + if (pipChange == null) { + return false; + } + + Rect startBounds = pipChange.getStartAbsBounds(); + Rect endBounds = pipChange.getEndAbsBounds(); + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); + + PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash, + startTransaction, startBounds, startBounds, endBounds, + PipEnterExitAnimator.BOUNDS_EXIT); + animator.setAnimationEndCallback(() -> { + finishCallback.onTransitionFinished(null); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + }); + + animator.start(); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 8df287d12cbc..06c57bd7092d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -113,6 +113,9 @@ public interface SplitScreen { /** Called when device waking up finished. */ void onFinishedWakingUp(); + /** Called when device starts going to sleep (screen off). */ + void onStartedGoingToSleep(); + /** Called when requested to go to fullscreen from the current active split app. */ void goToFullscreenFromSplit(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index e659151fee7f..b8575565ef8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -50,6 +50,7 @@ import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; @@ -180,6 +181,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final LauncherApps mLauncherApps; private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; private final SplitScreenImpl mImpl = new SplitScreenImpl(); private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; @@ -227,7 +229,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, Optional<DesktopTasksController> desktopTasksController, @Nullable StageCoordinator stageCoordinator, MultiInstanceHelper multiInstanceHelper, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Handler mainHandler) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; mTaskOrganizer = shellTaskOrganizer; @@ -236,6 +239,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mLauncherApps = context.getSystemService(LauncherApps.class); mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -292,7 +296,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider, - mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController, + mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController, mWindowDecorViewModel); } @@ -448,13 +452,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { - mStageCoordinator.onKeyguardVisibilityChanged(visible); + mStageCoordinator.onKeyguardStateChanged(visible, occluded); } public void onFinishedWakingUp() { mStageCoordinator.onFinishedWakingUp(); } + public void onStartedGoingToSleep() { + mStageCoordinator.onStartedGoingToSleep(); + } + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); } @@ -1201,6 +1209,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void onStartedGoingToSleep() { + mMainExecutor.execute(SplitScreenController.this::onStartedGoingToSleep); + } + + @Override public void goToFullscreenFromSplit() { mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index b3dab8527617..48d17ec6963f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -68,6 +68,7 @@ class SplitScreenTransitions { DismissSession mPendingDismiss = null; EnterSession mPendingEnter = null; TransitSession mPendingResize = null; + TransitSession mPendingRemotePassthrough = null; private IBinder mAnimatingTransition = null; private OneShotRemoteHandler mActiveRemoteHandler = null; @@ -320,6 +321,11 @@ class SplitScreenTransitions { return mPendingResize != null && mPendingResize.mTransition == transition; } + boolean isPendingPassThrough(IBinder transition) { + return mPendingRemotePassthrough != null && + mPendingRemotePassthrough.mTransition == transition; + } + @Nullable private TransitSession getPendingTransition(IBinder transition) { if (isPendingEnter(transition)) { @@ -331,6 +337,9 @@ class SplitScreenTransitions { } else if (isPendingResize(transition)) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "\tresolved resize transition"); return mPendingResize; + } else if (isPendingPassThrough(transition)) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "\tresolved passThrough transition"); + return mPendingRemotePassthrough; } return null; } @@ -378,6 +387,19 @@ class SplitScreenTransitions { extraTransitType, resizeAnim); } + /** Sets a transition to enter split. */ + void setRemotePassThroughTransition(@NonNull IBinder transition, + @Nullable RemoteTransition remoteTransition) { + mPendingRemotePassthrough = new TransitSession( + transition, null, null, + remoteTransition, Transitions.TRANSIT_SPLIT_PASSTHROUGH); + + ProtoLog.v(WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced remote passthrough split screen"); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setRemotePassThrough: transitType=%d remote=%s", + Transitions.TRANSIT_SPLIT_PASSTHROUGH, remoteTransition); + } + /** Starts a transition to dismiss split. */ IBinder startDismissTransition(WindowContainerTransaction wct, Transitions.TransitionHandler handler, @SplitScreen.StageType int dismissTop, @@ -474,6 +496,12 @@ class SplitScreenTransitions { mPendingResize.onConsumed(aborted); mPendingResize = null; ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for resize transition"); + } else if (isPendingPassThrough(transition)) { + mPendingRemotePassthrough.onConsumed(aborted); + mPendingRemotePassthrough.mRemoteHandler.onTransitionConsumed(transition, aborted, + finishT); + mPendingRemotePassthrough = null; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for passThrough transition"); } // TODO: handle transition consumed for active remote handler @@ -495,6 +523,10 @@ class SplitScreenTransitions { mPendingResize.onFinished(wct, mFinishTransaction); mPendingResize = null; ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinish for resize transition"); + } else if (isPendingPassThrough(mAnimatingTransition)) { + mPendingRemotePassthrough.onFinished(wct, mFinishTransaction); + mPendingRemotePassthrough = null; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinish for passThrough transition"); } mActiveRemoteHandler = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 99bd96e722fb..d7ee563b562c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -16,7 +16,7 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -92,6 +92,7 @@ import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.Debug; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; @@ -119,6 +120,7 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; +import com.android.internal.policy.FoldLockSettingsObserver; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ArrayUtils; import com.android.launcher3.icons.IconProvider; @@ -191,7 +193,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitLayout mSplitLayout; private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; - private boolean mKeyguardShowing; + private boolean mKeyguardActive; private boolean mShowDecorImmediately; private final SyncTransactionQueue mSyncQueue; private final ShellTaskOrganizer mTaskOrganizer; @@ -205,6 +207,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; // Cache live tile tasks while entering recents, evict them from stages in finish transaction // if user is opening another task(s). private final ArrayList<Integer> mPausingTasks = new ArrayList<>(); @@ -233,7 +236,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mIsExiting; private boolean mIsRootTranslucent; @VisibleForTesting - int mTopStageAfterFoldDismiss; + @StageType int mLastActiveStage; + private boolean mBreakOnNextWake; + /** Used to get the Settings value for "Continue using apps on fold". */ + private FoldLockSettingsObserver mFoldLockSettingsObserver; private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; @@ -313,9 +319,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, - IconProvider iconProvider, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, + TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; @@ -324,6 +329,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer = taskOrganizer; mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -366,6 +372,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // With shell transition, we should update recents tile each callback so set this to true by // default. mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS; + mFoldLockSettingsObserver = + new FoldLockSettingsObserver(mainHandler, context); + mFoldLockSettingsObserver.register(); } @VisibleForTesting @@ -373,9 +382,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, - Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, + Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; @@ -393,6 +401,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, this::onTransitionAnimationComplete, this); mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -400,6 +409,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); mSplitUnsupportedToast = Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); + mFoldLockSettingsObserver = + new FoldLockSettingsObserver(context.getMainThreadHandler(), context); + mFoldLockSettingsObserver.register(); } public void setMixedHandler(DefaultMixedHandler mixedHandler) { @@ -1504,51 +1516,80 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; + /** + * Runs when keyguard state changes. The booleans here are a bit complicated, so for reference: + * @param active {@code true} if we are in a state where the keyguard *should* be shown + * -- still true when keyguard is "there" but is behind an app, or + * screen is off. + * @param occludingTaskRunning {@code true} when there is a running task that has + * FLAG_SHOW_WHEN_LOCKED -- also true when the task is + * just running on its own and keyguard is not active + * at all. + */ + void onKeyguardStateChanged(boolean active, boolean occludingTaskRunning) { + mKeyguardActive = active; if (!mMainStage.isActive()) { return; } - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onKeyguardVisibilityChanged: showing=%b", showing); - setDividerVisibility(!mKeyguardShowing, null); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "onKeyguardVisibilityChanged: active=%b occludingTaskRunning=%b", + active, occludingTaskRunning); + setDividerVisibility(!mKeyguardActive, null); + + if (active && occludingTaskRunning) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + } } void onFinishedWakingUp() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinishedWakingUp"); - if (!mMainStage.isActive()) { + if (mBreakOnNextWake) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_DEVICE_FOLDED); + } + } + + void onStartedGoingToSleep() { + recordLastActiveStage(); + } + + /** + * Records the user's last focused stage -- main stage or side stage. Used to determine which + * stage of a split pair should be kept, in cases where system focus has moved elsewhere. + */ + void recordLastActiveStage() { + if (!isSplitActive() || !isSplitScreenVisible()) { + mLastActiveStage = STAGE_TYPE_UNDEFINED; + } else if (mMainStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_SIDE; + } + } + + /** + * Dismisses split, keeping the app that the user focused last in split screen. If the user was + * not in split screen, {@link #mLastActiveStage} should be set to STAGE_TYPE_UNDEFINED, and we + * will do a no-op. + */ + void dismissSplitKeepingLastActiveStage(@ExitReason int reason) { + if (!mMainStage.isActive() || mLastActiveStage == STAGE_TYPE_UNDEFINED) { + // no-op return; } - // Check if there's only one stage visible while keyguard occluded. - final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible; - final boolean oneStageVisible = - mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible; - if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) { - // Dismiss split because there's show-when-locked activity showing on top of keyguard. - // Also make sure the task contains show-when-locked activity remains on top after split - // dismissed. - final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage; - exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - } - - // Dismiss split if the flag record any side of stages. - if (mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { - if (ENABLE_SHELL_TRANSITIONS) { - // Need manually clear here due to this transition might be aborted due to keyguard - // on top and lead to no visible change. - clearSplitPairedInRecents(EXIT_REASON_DEVICE_FOLDED); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); - mSplitTransitions.startDismissTransition(wct, this, - mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); - setSplitsVisible(false); - } else { - exitSplitScreen( - mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, - EXIT_REASON_DEVICE_FOLDED); - } - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (ENABLE_SHELL_TRANSITIONS) { + // Need manually clear here due to this transition might be aborted due to keyguard + // on top and lead to no visible change. + clearSplitPairedInRecents(reason); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(mLastActiveStage, wct); + mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason); + setSplitsVisible(false); + } else { + exitSplitScreen(mLastActiveStage == STAGE_TYPE_MAIN ? mMainStage : mSideStage, reason); } + + mBreakOnNextWake = false; } void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { @@ -1909,8 +1950,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. - options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; // this might have to be changed as more split-to-pip cujs are defined. @@ -2223,11 +2264,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setDividerVisibility: visible=%b keyguardShowing=%b dividerAnimating=%b caller=%s", - visible, mKeyguardShowing, mIsDividerRemoteAnimating, Debug.getCaller()); + visible, mKeyguardActive, mIsDividerRemoteAnimating, Debug.getCaller()); // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard // dismissing animation. - if (visible && mKeyguardShowing) { + if (visible && mKeyguardActive) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, " Defer showing divider bar due to keyguard showing."); return; @@ -2597,21 +2638,24 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting void onFoldedStateChanged(boolean folded) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFoldedStateChanged: folded=%b", folded); - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; - if (!folded) return; - - if (!isSplitActive() || !isSplitScreenVisible()) return; - // To avoid split dismiss when user fold the device and unfold to use later, we only - // record the flag here and try to dismiss on wakeUp callback to ensure split dismiss - // when user interact on phone folded. - if (mMainStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; - } else if (mSideStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; + if (folded) { + recordLastActiveStage(); + // If user folds and has the setting "Continue using apps on fold = NEVER", we assume + // they don't want to continue using split on the outer screen (i.e. we break split if + // they wake the device in its folded state). + mBreakOnNextWake = willSleepOnFold(); + } else { + mBreakOnNextWake = false; } } + /** Returns true if the phone will sleep when it folds. */ + @VisibleForTesting + boolean willSleepOnFold() { + return mFoldLockSettingsObserver != null && mFoldLockSettingsObserver.isSleepOnFold(); + } + private Rect getSideStageBounds() { return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); @@ -2666,7 +2710,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Nullable TransitionRequestInfo request) { final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); if (triggerTask == null) { - if (isSplitScreenVisible()) { + if (isSplitActive()) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation", request.getDebugId()); // Check if the display is rotating. @@ -2676,6 +2720,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, && displayChange.getStartRotation() != displayChange.getEndRotation()) { mSplitLayout.setFreezeDividerWindow(true); } + if (request.getRemoteTransition() != null) { + mSplitTransitions.setRemotePassThroughTransition(transition, + request.getRemoteTransition()); + } // Still want to monitor everything while in split-screen, so return non-null. return new WindowContainerTransaction(); } else { @@ -3002,6 +3050,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, notifySplitAnimationFinished(); return true; } + } else if (mSplitTransitions.isPendingPassThrough(transition)) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "startAnimation: passThrough transition=%d", info.getDebugId()); + mSplitTransitions.mPendingRemotePassthrough.mRemoteHandler.startAnimation(transition, + info, startTransaction, finishTransaction, finishCallback); + notifySplitAnimationFinished(); + return true; } return startPendingAnimation(transition, info, startTransaction, finishTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java index e330f3ab65ab..b65e97899f3e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java @@ -33,7 +33,6 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.StageCoordinator; @@ -88,7 +87,8 @@ public class TvSplitScreenController extends SplitScreenController { syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, null, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, Optional.empty(), - Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor); + Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor, + mainHandler); mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java index 79476919221e..81ca48fa6b3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -56,7 +56,7 @@ public class TvStageCoordinator extends StageCoordinator SystemWindows systemWindows) { super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, - mainExecutor, recentTasks, launchAdjacentController, Optional.empty()); + mainExecutor, mainHandler, recentTasks, launchAdjacentController, Optional.empty()); mTvSplitMenuController = new TvSplitMenuController(context, this, systemWindows, mainHandler); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java index 5c814dcc9b16..bad5baf24651 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -73,7 +73,7 @@ class WindowlessSnapshotWindowCreator { final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId); final StartingSurfaceDrawer.WindowlessStartingWindow wlw = new StartingSurfaceDrawer.WindowlessStartingWindow( - runningTaskInfo.configuration, rootSurface); + mContext.getResources().getConfiguration(), rootSurface); final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost( mContext, display, wlw, "WindowlessSnapshotWindowCreator"); final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java index 98a803128587..f3725579bf48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java @@ -76,7 +76,7 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { } final StartingSurfaceDrawer.WindowlessStartingWindow wlw = new StartingSurfaceDrawer.WindowlessStartingWindow( - taskInfo.configuration, rootSurface); + mContext.getResources().getConfiguration(), rootSurface); final SurfaceControlViewHost viewHost = new SurfaceControlViewHost( myContext, display, wlw, "WindowlessSplashWindowCreator"); final String title = "Windowless Splash " + taskInfo.taskId; @@ -95,7 +95,7 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { } final FrameLayout rootLayout = new FrameLayout( - mSplashscreenContentDrawer.createViewContextWrapper(mContext)); + mSplashscreenContentDrawer.createViewContextWrapper(myContext)); viewHost.setView(rootLayout, lp); final int bgColor = taskDescription.getBackgroundColor(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java index dd4595a70211..287e779d8e24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -48,6 +48,7 @@ public class ShellInit { public ShellInit(ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; + ProtoLog.registerGroups(ShellProtoLogGroup.values()); } /** @@ -76,7 +77,6 @@ public class ShellInit { */ @VisibleForTesting public void init() { - ProtoLog.registerGroups(ShellProtoLogGroup.values()); ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size()); SurfaceControl.setDebugUsageAfterRelease(true); // Init in order of registration diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index fc8b1d27ad02..874cca523ad1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -190,6 +190,9 @@ public class Transitions implements RemoteCallable<Transitions>, // TRANSIT_FIRST_CUSTOM + 17 TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE; + /** Remote Transition that split accepts but ultimately needs to be animated by the remote. */ + public static final int TRANSIT_SPLIT_PASSTHROUGH = TRANSIT_FIRST_CUSTOM + 18; + /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index b9cb6d3d5007..5c230c0a505c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -56,6 +56,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -72,6 +73,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final IWindowManager mWindowManager; private final Context mContext; private final Handler mMainHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; @@ -108,6 +110,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { public CaptionWindowDecorViewModel( Context context, Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellExecutor shellExecutor, Choreographer mainChoreographer, IWindowManager windowManager, @@ -120,6 +123,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; + mBgExecutor = bgExecutor; mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; @@ -289,6 +293,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { taskInfo, taskSurface, mMainHandler, + mBgExecutor, mMainChoreographer, mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 7e1b973a98f4..cf42a49f7103 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLarge import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; @@ -48,7 +49,9 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** @@ -58,6 +61,7 @@ import com.android.wm.shell.windowdecor.extension.TaskInfoKt; */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private final Handler mHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -78,10 +82,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue) { super(context, displayController, taskOrganizer, taskInfo, taskSurface); mHandler = handler; + mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; } @@ -218,6 +224,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL relayoutParams.mOccludingCaptionElements.add(controlsElement); } + @SuppressLint("MissingPermission") void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { @@ -235,7 +242,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo - mTaskOrganizer.applyTransaction(wct); + mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 53976251e31d..0e8fd7c7d0a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -22,6 +22,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.Intent.ACTION_MAIN; +import static android.content.Intent.CATEGORY_APP_BROWSER; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; @@ -36,6 +38,8 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; @@ -43,11 +47,8 @@ import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; @@ -98,6 +99,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; @@ -132,6 +134,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final ShellController mShellController; private final Context mContext; private final Handler mMainHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; @@ -183,6 +186,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { ShellExecutor shellExecutor, Handler mainHandler, Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, IWindowManager windowManager, @@ -201,6 +205,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { shellExecutor, mainHandler, mainChoreographer, + bgExecutor, shellInit, shellCommandHandler, windowManager, @@ -225,6 +230,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { ShellExecutor shellExecutor, Handler mainHandler, Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, IWindowManager windowManager, @@ -245,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mMainExecutor = shellExecutor; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; + mBgExecutor = bgExecutor; mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); mTaskOrganizer = taskOrganizer; mShellController = shellController; @@ -321,7 +328,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (decoration == null) return; final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo; - if (taskInfo.displayId != oldTaskInfo.displayId) { + if (taskInfo.displayId != oldTaskInfo.displayId + && !Flags.enableAdditionalWindowsAboveStatusBar()) { removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } @@ -385,7 +393,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.close(); final int displayId = taskInfo.displayId; - if (mEventReceiversByDisplay.contains(displayId)) { + if (mEventReceiversByDisplay.contains(displayId) + && !Flags.enableAdditionalWindowsAboveStatusBar()) { removeTaskFromEventReceiver(displayId); } // Remove the decoration from the cache last because WindowDecoration#close could still @@ -424,19 +433,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } private void openInBrowser(Uri uri) { - final Intent intent = new Intent(Intent.ACTION_VIEW, uri) - .setComponent(getDefaultBrowser()) + final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER) + .setData(uri) .addFlags(FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } - private ComponentName getDefaultBrowser() { - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); - final ResolveInfo info = mContext.getPackageManager() - .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); - return info.getComponentInfo().getComponentName(); - } - private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -517,6 +519,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); + // When the app enters split-select, the handle will no longer be visible, meaning + // we shouldn't receive input for it any longer. + decoration.disposeStatusBarInputLayer(); mDesktopTasksController.requestSplit(decoration.mTaskInfo); } else if (id == R.id.open_in_browser_button) { // TODO(b/346441962): let the decoration handle the click gesture and only call back @@ -653,13 +658,38 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final RunningTaskInfo taskInfo = decoration.mTaskInfo; if (DesktopModeStatus.canEnterDesktopMode(mContext) - && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - return false; + && !taskInfo.isFreeform()) { + return handleNonFreeformMotionEvent(decoration, v, e); + } else { + return handleFreeformMotionEvent(decoration, taskInfo, v, e); } + } + + private boolean handleNonFreeformMotionEvent(DesktopModeWindowDecoration decoration, + View v, MotionEvent e) { + final int id = v.getId(); + if (id == R.id.caption_handle) { + if (e.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Caption handle is located within the status bar region, meaning the + // DisplayPolicy will attempt to transfer this input to status bar if it's + // a swipe down. Pilfer here to keep the gesture in handle alone. + mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); + } + handleCaptionThroughStatusBar(e, decoration); + final boolean wasDragging = mIsDragging; + updateDragStatus(e.getActionMasked()); + // Only prevent onClick from receiving this event if it's a drag. + return wasDragging; + } + return false; + } + + private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration, + RunningTaskInfo taskInfo, View v, MotionEvent e) { + final int id = v.getId(); if (mGestureDetector.onTouchEvent(e)) { return true; } - final int id = v.getId(); final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window || id == R.id.open_menu_button); switch (e.getActionMasked()) { @@ -668,7 +698,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - mIsDragging = false; + updateDragStatus(e.getActionMasked()); mHasLongClicked = false; // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. @@ -688,7 +718,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.mTaskSurface, e.getRawX(dragPointerIdx), newTaskBounds); - mIsDragging = true; + updateDragStatus(e.getActionMasked()); return true; } case MotionEvent.ACTION_UP: @@ -718,7 +748,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // onClick call that results. return false; } else { - mIsDragging = false; + updateDragStatus(e.getActionMasked()); return true; } } @@ -726,6 +756,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } + private void updateDragStatus(int eventAction) { + switch (eventAction) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + mIsDragging = false; + break; + } + case MotionEvent.ACTION_MOVE: { + mIsDragging = true; + break; + } + } + } + /** * Perform a task size toggle on release of the double-tap, assuming no drag event * was handled during the double-tap. @@ -850,6 +895,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { * * @param relevantDecor the window decoration of the focused task's caption. This method only * handles motion events outside this caption's bounds. + * TODO(b/349135068): Outside-touch detection no longer works with the + * enableAdditionalWindowsAboveStatusBar flag enabled. This + * will be fixed once we can add FLAG_WATCH_OUTSIDE_TOUCH to relevant menus, + * at which point, all EventReceivers and external touch logic should be removed. */ private void handleEventOutsideCaption(MotionEvent ev, DesktopModeWindowDecoration relevantDecor) { @@ -902,9 +951,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } - - if (dragFromStatusBarAllowed - && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) { + final boolean shouldStartTransitionDrag = + relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) + || Flags.enableAdditionalWindowsAboveStatusBar(); + if (dragFromStatusBarAllowed && shouldStartTransitionDrag) { mTransitionDragActive = true; } break; @@ -918,8 +968,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // Though this isn't a hover event, we need to update handle's hover state // as it likely will change. relevantDecor.updateHoverAndPressStatus(ev); - mDesktopTasksController.onDragPositioningEndThroughStatusBar( - new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo); + DesktopModeVisualIndicator.IndicatorType resultType = + mDesktopTasksController.onDragPositioningEndThroughStatusBar( + new PointF(ev.getRawX(), ev.getRawY()), + relevantDecor.mTaskInfo, + relevantDecor.mTaskSurface); + // If we are entering split select, handle will no longer be visible and + // should not be receiving any input. + if (resultType == TO_SPLIT_LEFT_INDICATOR + || resultType != TO_SPLIT_RIGHT_INDICATOR) { + relevantDecor.disposeStatusBarInputLayer(); + } mMoveToDesktopAnimator = null; return; } else { @@ -931,7 +990,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { relevantDecor.checkTouchEvent(ev); break; } - case ACTION_MOVE: { if (relevantDecor == null) { return; @@ -954,7 +1012,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mContext, mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo, relevantDecor.mTaskSurface); mDesktopTasksController.startDragToDesktop(relevantDecor.mTaskInfo, - mMoveToDesktopAnimator); + mMoveToDesktopAnimator, relevantDecor.mTaskSurface); } } if (mMoveToDesktopAnimator != null) { @@ -1091,10 +1149,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeWindowDecorFactory.create( mContext, mDisplayController, + mSplitScreenController, mTaskOrganizer, taskInfo, taskSurface, mMainHandler, + mBgExecutor, mMainChoreographer, mSyncQueue, mRootTaskDisplayAreaOrganizer); @@ -1132,7 +1192,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { windowDecoration.setDragDetector(touchEventListener.mDragDetector); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */); - incrementEventReceiverTasks(taskInfo.displayId); + if (!Flags.enableAdditionalWindowsAboveStatusBar()) { + incrementEventReceiverTasks(taskInfo.displayId); + } } private RunningTaskInfo getOtherSplitTask(int taskId) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 5d662b20ebb9..529def7ca3d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -24,11 +24,13 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; import android.content.ComponentName; @@ -68,7 +70,9 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; @@ -95,8 +99,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; private final Handler mHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; + private final SplitScreenController mSplitScreenController; private WindowDecorationViewHolder mWindowDecorViewHolder; private View.OnClickListener mOnCaptionButtonClickListener; @@ -147,27 +153,32 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DesktopModeWindowDecoration( Context context, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - this (context, displayController, taskOrganizer, taskInfo, taskSurface, - handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, - SurfaceControl.Builder::new, SurfaceControl.Transaction::new, - WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE); + this (context, displayController, splitScreenController, taskOrganizer, taskInfo, + taskSurface, handler, bgExecutor, choreographer, syncQueue, + rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, + SurfaceControl.Transaction::new, WindowContainerTransaction::new, + SurfaceControl::new, new SurfaceControlViewHostFactory() {}, + DefaultMaximizeMenuFactory.INSTANCE); } DesktopModeWindowDecoration( Context context, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @@ -181,7 +192,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, surfaceControlViewHostFactory); + mSplitScreenController = splitScreenController; mHandler = handler; + mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; @@ -327,6 +340,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandler.post(mCurrentViewHostRunnable); } + @SuppressLint("MissingPermission") private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { @@ -337,7 +351,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } if (isHandleMenuActive()) { - mHandleMenu.relayout(startT); + mHandleMenu.relayout(startT, mResult.mCaptionX); } updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, @@ -353,33 +367,47 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT"); - mTaskOrganizer.applyTransaction(wct); + mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); Trace.endSection(); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. + disposeStatusBarInputLayer(); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces return; } if (oldRootView != mResult.mRootView) { + disposeStatusBarInputLayer(); mWindowDecorViewHolder = createViewHolder(); } Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); - mWindowDecorViewHolder.bindData(mTaskInfo); + + final Point position = new Point(); + if (isAppHandle(mWindowDecorViewHolder)) { + position.set(determineHandlePosition()); + } + mWindowDecorViewHolder.bindData(mTaskInfo, + position, + mResult.mCaptionWidth, + mResult.mCaptionHeight, + isCaptionVisible()); Trace.endSection(); if (!mTaskInfo.isFocused) { closeHandleMenu(); closeMaximizeMenu(); } - updateDragResizeListener(oldDecorationSurface); updateMaximizeMenu(startT); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces } + private boolean isCaptionVisible() { + return mTaskInfo.isVisible && mIsCaptionVisible; + } + private void setCapturedLink(Uri capturedLink, long timeStamp) { if (capturedLink == null || (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) { @@ -461,12 +489,42 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } + private Point determineHandlePosition() { + final Point position = new Point(mResult.mCaptionX, 0); + if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) + == SPLIT_POSITION_BOTTOM_OR_RIGHT + && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape() + ) { + // If this is the right split task, add left stage's width. + final Rect leftStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); + position.x += leftStageBounds.width(); + } + return position; + } + + /** + * Dispose of the view used to forward inputs in status bar region. Intended to be + * used any time handle is no longer visible. + */ + void disposeStatusBarInputLayer() { + if (!isAppHandle(mWindowDecorViewHolder) + || !Flags.enableAdditionalWindowsAboveStatusBar()) { + return; + } + ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer(); + } + private WindowDecorationViewHolder createViewHolder() { if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) { return new AppHandleViewHolder( mResult.mRootView, mOnCaptionTouchListener, - mOnCaptionButtonClickListener + mOnCaptionButtonClickListener, + (v, event) -> { + updateHoverAndPressStatus(event); + return true; + } ); } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_header) { @@ -489,6 +547,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin throw new IllegalArgumentException("Unexpected layout resource id"); } + private boolean isAppHandle(WindowDecorationViewHolder viewHolder) { + return viewHolder instanceof AppHandleViewHolder; + } + @VisibleForTesting static void updateRelayoutParams( RelayoutParams relayoutParams, @@ -863,7 +925,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin splitScreenController, DesktopModeStatus.canEnterDesktopMode(mContext), browserLinkAvailable(), - mResult.mCaptionHeight + mResult.mCaptionWidth, + mResult.mCaptionHeight, + mResult.mCaptionX ); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); @@ -955,10 +1019,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @return {@code true} if event is inside caption handle view, {@code false} if not */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { - if (isHandleMenuActive() || !(mWindowDecorViewHolder - instanceof AppHandleViewHolder)) { + if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder) + || Flags.enableAdditionalWindowsAboveStatusBar()) { return false; } + // The status bar input layer can only receive input in handle coordinates to begin with, + // so checking coordinates is unnecessary as input is always within handle bounds. + if (isAppHandle(mWindowDecorViewHolder) + && Flags.enableAdditionalWindowsAboveStatusBar() + && isCaptionVisible()) { + return true; + } return checkTouchEventInCaption(ev); } @@ -992,7 +1063,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare */ void checkTouchEvent(MotionEvent ev) { - if (mResult.mRootView == null) return; + if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return; final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); final View handle = caption.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() @@ -1014,7 +1085,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare against. */ void updateHoverAndPressStatus(MotionEvent ev) { - if (mResult.mRootView == null) return; + if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return; final View handle = mResult.mRootView.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() && checkTouchEventInFocusedCaptionHandle(ev); @@ -1024,15 +1095,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // We want handle to remain pressed if the pointer moves outside of it during a drag. handle.setPressed((inHandle && action == ACTION_DOWN) || (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL)); - if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) { + if (isHandleMenuActive()) { mHandleMenu.checkMotionEvent(ev); } } - private boolean isHandleMenuAboveStatusBar() { - return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform(); - } - private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; @@ -1044,6 +1111,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); + disposeStatusBarInputLayer(); clearCurrentViewHostRunnable(); super.close(); } @@ -1143,20 +1211,24 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DesktopModeWindowDecoration create( Context context, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { return new DesktopModeWindowDecoration( context, displayController, + splitScreenController, taskOrganizer, taskInfo, taskSurface, handler, + bgExecutor, choreographer, syncQueue, rootTaskDisplayAreaOrganizer); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index da268988bac7..3fd3656ccbc5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -19,7 +19,11 @@ package com.android.wm.shell.windowdecor; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; 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_HOVER_EXIT; +import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; import android.graphics.PointF; @@ -43,7 +47,7 @@ class DragDetector { private final PointF mInputDownPoint = new PointF(); private int mTouchSlop; private boolean mIsDragEvent; - private int mDragPointerId; + private int mDragPointerId = -1; private boolean mResultOfDownAction; @@ -67,7 +71,7 @@ class DragDetector { * * @return the result returned by {@link #mEventHandler}, or the result when * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed - */ + */ boolean onMotionEvent(View v, MotionEvent ev) { final boolean isTouchScreen = (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; @@ -86,10 +90,14 @@ class DragDetector { return mResultOfDownAction; } case ACTION_MOVE: { - if (ev.findPointerIndex(mDragPointerId) == -1) { - mDragPointerId = ev.getPointerId(0); + if (mDragPointerId == -1) { + // The primary pointer was lifted, ignore the rest of the gesture. + return mResultOfDownAction; } final int dragPointerIndex = ev.findPointerIndex(mDragPointerId); + if (dragPointerIndex == -1) { + throw new IllegalStateException("Failed to find primary pointer!"); + } if (!mIsDragEvent) { float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; @@ -99,22 +107,52 @@ class DragDetector { } // The event handler should only be notified about 'move' events if a drag has been // detected. - if (mIsDragEvent) { - return mEventHandler.handleMotionEvent(v, ev); - } else { + if (!mIsDragEvent) { return mResultOfDownAction; } + return mEventHandler.handleMotionEvent(v, + getSinglePointerEvent(ev, mDragPointerId)); + } + case ACTION_HOVER_ENTER: + case ACTION_HOVER_MOVE: + case ACTION_HOVER_EXIT: { + return mEventHandler.handleMotionEvent(v, + getSinglePointerEvent(ev, mDragPointerId)); + } + case ACTION_POINTER_UP: { + if (mDragPointerId == -1) { + // The primary pointer was lifted, ignore the rest of the gesture. + return mResultOfDownAction; + } + if (mDragPointerId != ev.getPointerId(ev.getActionIndex())) { + // Ignore a secondary pointer being lifted. + return mResultOfDownAction; + } + // The primary pointer is being lifted. + final int dragPointerId = mDragPointerId; + mDragPointerId = -1; + return mEventHandler.handleMotionEvent(v, getSinglePointerEvent(ev, dragPointerId)); } case ACTION_UP: case ACTION_CANCEL: { + final int dragPointerId = mDragPointerId; resetState(); - return mEventHandler.handleMotionEvent(v, ev); + if (dragPointerId == -1) { + // The primary pointer was lifted, ignore the rest of the gesture. + return mResultOfDownAction; + } + return mEventHandler.handleMotionEvent(v, getSinglePointerEvent(ev, dragPointerId)); } default: - return mEventHandler.handleMotionEvent(v, ev); + // Ignore other events. + return mResultOfDownAction; } } + private static MotionEvent getSinglePointerEvent(MotionEvent ev, int pointerId) { + return ev.getPointerCount() > 1 ? ev.split(1 << pointerId) : ev; + } + void setTouchSlop(int touchSlop) { mTouchSlop = touchSlop; } @@ -129,4 +167,4 @@ class DragDetector { interface MotionEventHandler { boolean handleMotionEvent(@Nullable View v, MotionEvent ev); } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index b51b700bc315..e174e83d11c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -69,7 +69,9 @@ class HandleMenu( private val splitScreenController: SplitScreenController, private val shouldShowWindowingPill: Boolean, private val shouldShowBrowserPill: Boolean, - private val captionHeight: Int + private val captionWidth: Int, + private val captionHeight: Int, + captionX: Int ) { private val context: Context = parentDecor.mDecorWindowContext private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo @@ -105,7 +107,7 @@ class HandleMenu( private val globalMenuPosition: Point = Point() init { - updateHandleMenuPillPositions() + updateHandleMenuPillPositions(captionX) } fun show() { @@ -262,11 +264,11 @@ class HandleMenu( /** * Updates handle menu's position variables to reflect its next position. */ - private fun updateHandleMenuPillPositions() { + private fun updateHandleMenuPillPositions(captionX: Int) { val menuX: Int val menuY: Int val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds - updateGlobalMenuPosition(taskBounds) + updateGlobalMenuPosition(taskBounds, captionX) if (layoutResId == R.layout.desktop_mode_app_header) { // Align the handle menu to the left side of the caption. menuX = marginMenuStart @@ -287,7 +289,8 @@ class HandleMenu( handleMenuPosition.set(menuX.toFloat(), menuY.toFloat()) } - private fun updateGlobalMenuPosition(taskBounds: Rect) { + private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) { + val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2) when { taskInfo.isFreeform -> { globalMenuPosition.set( @@ -297,7 +300,7 @@ class HandleMenu( } taskInfo.isFullscreen -> { globalMenuPosition.set( - /* x = */ taskBounds.width() / 2 - (menuWidth / 2), + /* x = */ nonFreeformX, /* y = */ marginMenuTop ) } @@ -311,16 +314,13 @@ class HandleMenu( when (splitPosition) { SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { globalMenuPosition.set( - /* x = */ leftOrTopStageBounds.width() - + (rightOrBottomStageBounds.width() / 2) - - (menuWidth / 2), + /* x = */ leftOrTopStageBounds.width() + nonFreeformX, /* y = */ marginMenuTop ) } SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { globalMenuPosition.set( - /* x = */ (leftOrTopStageBounds.width() / 2) - - (menuWidth / 2), + /* x = */ nonFreeformX, /* y = */ marginMenuTop ) } @@ -332,9 +332,12 @@ class HandleMenu( /** * Update pill layout, in case task changes have caused positioning to change. */ - fun relayout(t: SurfaceControl.Transaction) { + fun relayout( + t: SurfaceControl.Transaction, + captionX: Int + ) { handleMenuViewContainer?.let { container -> - updateHandleMenuPillPositions() + updateHandleMenuPillPositions(captionX) container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index d212f2131ed4..a691f59a2155 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -139,7 +139,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private SurfaceControlViewHost mViewHost; private Configuration mWindowDecorConfig; TaskDragResizer mTaskDragResizer; - private boolean mIsCaptionVisible; + boolean mIsCaptionVisible; /** The most recent set of insets applied to this window decoration. */ private WindowDecorationInsets mWindowDecorationInsets; @@ -508,6 +508,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTaskDragResizer = taskDragResizer; } + // TODO(b/346441962): Move these three methods closer to implementing or View-level classes to + // keep implementation details more encapsulated. private void setCaptionVisibility(View rootView, boolean visible) { if (rootView == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 4897f76a20cf..6a354f10049b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -30,17 +30,22 @@ import android.view.WindowManager */ class AdditionalSystemViewContainer( private val context: Context, - layoutId: Int, taskId: Int, x: Int, y: Int, width: Int, - height: Int + height: Int, + layoutId: Int? = null ) : AdditionalViewContainer() { override val view: View + val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java) init { - view = LayoutInflater.from(context).inflate(layoutId, null) + if (layoutId != null) { + view = LayoutInflater.from(context).inflate(layoutId, null) + } else { + view = View(context) + } val lp = WindowManager.LayoutParams( width, height, x, y, WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, @@ -51,12 +56,11 @@ class AdditionalSystemViewContainer( gravity = Gravity.LEFT or Gravity.TOP setTrustedOverlay() } - val wm: WindowManager? = context.getSystemService(WindowManager::class.java) - wm?.addView(view, lp) + windowManager?.addView(view, lp) } override fun releaseView() { - context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view) + windowManager?.removeViewImmediate(view) } override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { @@ -64,6 +68,6 @@ class AdditionalSystemViewContainer( this.x = x.toInt() this.y = y.toInt() } - view.layoutParams = lp + windowManager?.updateViewLayout(view, lp) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 8d822c252288..76dfe37c7cc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -20,37 +20,68 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.Point +import android.view.SurfaceControl import android.view.View +import android.view.View.OnClickListener +import android.view.View.OnHoverListener import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import android.view.WindowManager import android.widget.ImageButton +import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. */ internal class AppHandleViewHolder( - rootView: View, - onCaptionTouchListener: View.OnTouchListener, - onCaptionButtonClickListener: View.OnClickListener + rootView: View, + private val onCaptionTouchListener: View.OnTouchListener, + private val onCaptionButtonClickListener: OnClickListener, + private val onCaptionHoverListener: OnHoverListener, ) : WindowDecorationViewHolder(rootView) { companion object { private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 } - + private lateinit var taskInfo: RunningTaskInfo + private val windowManager = context.getSystemService(WindowManager::class.java) private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) + // An invisible View that takes up the same coordinates as captionHandle but is layered + // above the status bar. The purpose of this View is to receive input intended for + // captionHandle. + private var statusBarInputLayer: AdditionalSystemViewContainer? = null + init { captionView.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnClickListener(onCaptionButtonClickListener) + captionHandle.setOnHoverListener(onCaptionHoverListener) } - override fun bindData(taskInfo: RunningTaskInfo) { + override fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) + this.taskInfo = taskInfo + if (!isCaptionVisible && hasStatusBarInputLayer()) { + disposeStatusBarInputLayer() + return + } + if (hasStatusBarInputLayer()) { + updateStatusBarInputLayer(position) + } else { + createStatusBarInputLayer(position, width, height) + } } override fun onHandleMenuOpened() { @@ -61,6 +92,45 @@ internal class AppHandleViewHolder( animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } + private fun createStatusBarInputLayer(handlePosition: Point, + handleWidth: Int, + handleHeight: Int) { + if (!Flags.enableAdditionalWindowsAboveStatusBar()) return + statusBarInputLayer = AdditionalSystemViewContainer(context, taskInfo.taskId, + handlePosition.x, handlePosition.y, handleWidth, handleHeight) + val view = statusBarInputLayer?.view + val lp = view?.layoutParams as WindowManager.LayoutParams + lp.title = "Handle Input Layer of task " + taskInfo.taskId + lp.setTrustedOverlay() + // Make this window a spy window to enable it to pilfer pointers from the system-wide + // gesture listener that receives events before window. This is to prevent notification + // shade gesture when we swipe down to enter desktop. + lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + view.id = R.id.caption_handle + view.setOnClickListener(onCaptionButtonClickListener) + view.setOnTouchListener(onCaptionTouchListener) + view.setOnHoverListener(onCaptionHoverListener) + windowManager.updateViewLayout(view, lp) + } + + private fun updateStatusBarInputLayer(globalPosition: Point) { + statusBarInputLayer?.setPosition(SurfaceControl.Transaction(), globalPosition.x.toFloat(), + globalPosition.y.toFloat()) ?: return + } + + private fun hasStatusBarInputLayer(): Boolean { + return statusBarInputLayer != null + } + + /** + * Remove the input layer from [WindowManager]. Should be used when caption handle + * is not visible. + */ + fun disposeStatusBarInputLayer() { + statusBarInputLayer?.releaseView() + statusBarInputLayer = null + } + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 46127b177bc3..b704d9c001c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -21,6 +21,7 @@ import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Point import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable @@ -136,7 +137,13 @@ internal class AppHeaderViewHolder( onMaximizeHoverAnimationFinishedListener } - override fun bindData(taskInfo: RunningTaskInfo) { + override fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) { if (Flags.enableThemedAppHeaders()) { bindDataWithThemedHeaders(taskInfo) } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt index 5ae8d252a908..2341b099699f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo import android.content.Context +import android.graphics.Point import android.view.View /** @@ -30,7 +31,13 @@ internal abstract class WindowDecorationViewHolder(rootView: View) { * A signal to the view holder that new data is available and that the views should be updated to * reflect it. */ - abstract fun bindData(taskInfo: RunningTaskInfo) + abstract fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) /** Callback when the handle menu is opened. */ abstract fun onHandleMenuOpened() diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/SwitchToOverviewFromDesktop.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/SwitchToOverviewFromDesktop.kt new file mode 100644 index 000000000000..b4cadf4f300b --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/SwitchToOverviewFromDesktop.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +/** +* Base test for opening recent apps overview from desktop mode. +* +* Navigation mode can be passed as a constructor parameter, by default it is set to gesture navigation. +*/ +@Ignore("Base Test Class") +abstract class SwitchToOverviewFromDesktop +@JvmOverloads +constructor(val navigationMode: NavBar = NavBar.MODE_GESTURAL) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(navigationMode, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun switchToOverview() { + tapl.getLaunchedAppState().switchToOverview() + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 6b6954289a34..a0408652a29b 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -37,6 +37,7 @@ android_test { ], static_libs: [ + "TestParameterInjector", "WindowManager-Shell", "junit", "flag-junit", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index 55b6bd278f20..bba9418db66a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -46,12 +46,14 @@ import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.window.TransitionInfo; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -59,6 +61,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; +import java.util.Arrays; /** * Tests for {@link ActivityEmbeddingAnimationRunner}. @@ -67,7 +70,7 @@ import java.util.ArrayList; * atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests */ @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(TestParameterInjector.class) public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { @Rule @@ -204,15 +207,13 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim // TODO(b/243518738): Rewrite with TestParameter @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) @Test - public void testCalculateParentBounds_flagEnabled() { + public void testCalculateParentBounds_flagEnabled_emptyParentSize() { TransitionInfo.Change change; final TransitionInfo.Change stubChange = createChange(0 /* flags */); final Rect actualParentBounds = new Rect(); - Rect parentBounds = new Rect(0, 0, 2000, 2000); - Rect endAbsBounds = new Rect(0, 0, 2000, 2000); change = prepareChangeForParentBoundsCalculationTest( new Point(0, 0) /* endRelOffset */, - endAbsBounds, + new Rect(0, 0, 2000, 2000), new Point() /* endParentSize */ ); @@ -220,69 +221,80 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim assertTrue("Parent bounds must be empty because end parent size is not set.", actualParentBounds.isEmpty()); + } - String testString = "Parent start with (0, 0)"; - change = prepareChangeForParentBoundsCalculationTest( - new Point(endAbsBounds.left - parentBounds.left, - endAbsBounds.top - parentBounds.top), - endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); - - calculateParentBounds(change, stubChange, actualParentBounds); - - assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, - actualParentBounds); - - testString = "Container not start with (0, 0)"; - parentBounds = new Rect(0, 0, 2000, 2000); - endAbsBounds = new Rect(1000, 500, 2000, 1500); - change = prepareChangeForParentBoundsCalculationTest( - new Point(endAbsBounds.left - parentBounds.left, - endAbsBounds.top - parentBounds.top), - endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); - - calculateParentBounds(change, stubChange, actualParentBounds); - - assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, - actualParentBounds); - - testString = "Parent container on the right"; - parentBounds = new Rect(1000, 0, 2000, 2000); - endAbsBounds = new Rect(1000, 500, 1500, 1500); - change = prepareChangeForParentBoundsCalculationTest( - new Point(endAbsBounds.left - parentBounds.left, - endAbsBounds.top - parentBounds.top), - endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); - - calculateParentBounds(change, stubChange, actualParentBounds); - - assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, - actualParentBounds); - - testString = "Parent container on the bottom"; - parentBounds = new Rect(0, 1000, 2000, 2000); - endAbsBounds = new Rect(500, 1500, 1500, 2000); - change = prepareChangeForParentBoundsCalculationTest( + @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) + @Test + public void testCalculateParentBounds_flagEnabled( + @TestParameter ParentBoundsTestParameters params) { + final TransitionInfo.Change stubChange = createChange(0 /*flags*/); + final Rect parentBounds = params.getParentBounds(); + final Rect endAbsBounds = params.getEndAbsBounds(); + final TransitionInfo.Change change = prepareChangeForParentBoundsCalculationTest( new Point(endAbsBounds.left - parentBounds.left, endAbsBounds.top - parentBounds.top), endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + final Rect actualParentBounds = new Rect(); calculateParentBounds(change, stubChange, actualParentBounds); - assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, - actualParentBounds); - - testString = "Parent container in the middle"; - parentBounds = new Rect(500, 500, 1500, 1500); - endAbsBounds = new Rect(1000, 500, 1500, 1000); - change = prepareChangeForParentBoundsCalculationTest( - new Point(endAbsBounds.left - parentBounds.left, - endAbsBounds.top - parentBounds.top), - endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); - - calculateParentBounds(change, stubChange, actualParentBounds); + assertEquals(parentBounds, actualParentBounds); + } - assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds, - actualParentBounds); + private enum ParentBoundsTestParameters { + PARENT_START_WITH_0_0( + new int[]{0, 0, 2000, 2000}, + new int[]{0, 0, 2000, 2000}), + CONTAINER_NOT_START_WITH_0_0( + new int[] {0, 0, 2000, 2000}, + new int[] {1000, 500, 1500, 1500}), + PARENT_ON_THE_RIGHT( + new int[] {1000, 0, 2000, 2000}, + new int[] {1000, 500, 1500, 1500}), + PARENT_ON_THE_BOTTOM( + new int[] {0, 1000, 2000, 2000}, + new int[] {500, 1500, 1500, 2000}), + PARENT_IN_THE_MIDDLE( + new int[] {500, 500, 1500, 1500}, + new int[] {1000, 500, 1500, 1000}); + + /** + * An int array to present {left, top, right, bottom} of the parent {@link Rect bounds}. + */ + @NonNull + private final int[] mParentBounds; + + /** + * An int array to present {left, top, right, bottom} of the absolute container + * {@link Rect bounds} after the transition finishes. + */ + @NonNull + private final int[] mEndAbsBounds; + + ParentBoundsTestParameters( + @NonNull int[] parentBounds, @NonNull int[] endAbsBounds) { + mParentBounds = parentBounds; + mEndAbsBounds = endAbsBounds; + } + + @NonNull + private Rect getParentBounds() { + return asRect(mParentBounds); + } + + @NonNull + private Rect getEndAbsBounds() { + return asRect(mEndAbsBounds); + } + + @NonNull + private static Rect asRect(@NonNull int[] bounds) { + if (bounds.length != 4) { + throw new IllegalArgumentException("There must be exactly 4 elements in bounds, " + + "but found " + bounds.length + ": " + Arrays.toString(bounds)); + } + return new Rect(bounds[0], bounds[1], bounds[2], bounds[3]); + } } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 6002c21ccb24..37510ef4ba21 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -66,6 +66,7 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.MockToken @@ -166,6 +167,9 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator @Mock lateinit var recentTasksController: RecentTasksController + @Mock + private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockSurface: SurfaceControl private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -248,7 +252,8 @@ class DesktopTasksControllerTest : ShellTestCase() { multiInstanceHelper, shellExecutor, Optional.of(desktopTasksLimiter), - recentTasksController) + recentTasksController, + mockInteractionJankMonitor) } @After @@ -580,138 +585,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { - val task = setUpFullscreenTask() - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { - val task = - setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { - val task = - setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT, - shouldLetterbox = true) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -2148,7 +2021,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFullscreenTask() setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @@ -2164,7 +2037,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @@ -2181,7 +2054,7 @@ class DesktopTasksControllerTest : ShellTestCase() { setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) } @@ -2198,7 +2071,7 @@ class DesktopTasksControllerTest : ShellTestCase() { setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @@ -2218,7 +2091,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shouldLetterbox = true) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) } @@ -2234,7 +2107,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @@ -2253,7 +2126,7 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation = SCREEN_ORIENTATION_PORTRAIT) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @@ -2273,7 +2146,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shouldLetterbox = true) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) } @@ -2293,7 +2166,7 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation = SCREEN_ORIENTATION_PORTRAIT) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @@ -2314,7 +2187,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shouldLetterbox = true) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index bbf523bc40d2..e4e2bd216c94 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -15,6 +15,7 @@ import android.window.TransitionInfo import android.window.TransitionInfo.FLAG_IS_WALLPAPER import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest +import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder @@ -51,6 +52,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var splitScreenController: SplitScreenController @Mock private lateinit var dragAnimator: MoveToDesktopAnimator + @Mock + private lateinit var mockInteractionJankMonitor: InteractionJankMonitor private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -63,7 +66,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, - transactionSupplier + mockInteractionJankMonitor, + transactionSupplier, ) .apply { setSplitScreenController(splitScreenController) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 6888de5472e2..75d21457b60b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -182,7 +182,7 @@ public class PipControllerTest extends ShellTestCase { @Test public void instantiatePipController_registersPipTransitionCallback() { - verify(mMockPipTransitionController).registerPipTransitionCallback(any()); + verify(mMockPipTransitionController).registerPipTransitionCallback(any(), any()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index ace09a82d71c..66f8c0b9558d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -114,8 +114,8 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipDisplayLayoutState, mSizeSpecSource); - final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, - mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, + final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mMainExecutor, + mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator, Optional.empty() /* pipPerfHintControllerOptional */); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index 92762fa68550..6d18e3696f84 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -116,8 +116,8 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipSnapAlgorithm = new PipSnapAlgorithm(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, new PipKeepClearAlgorithmInterface() {}, mPipDisplayLayoutState, mSizeSpecSource); - PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState, - mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, + PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mMainExecutor, + mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator, Optional.empty() /* pipPerfHintControllerOptional */); mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt index b1d62f485a2a..dd19d76d88cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt @@ -184,108 +184,6 @@ class DesktopModeFlagsTest : ShellTestCase() { } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun isEnabled_noSystemProperty_overrideOn_featureFlagOff_returnsTrueAndStoresPropertyOn() { - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY) - setOverride(OVERRIDE_ON.setting) - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue() - // Store System Property if not present - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_ON.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun isEnabled_noSystemProperty_overrideUnset_featureFlagOn_returnsTrueAndStoresPropertyUnset() { - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY) - setOverride(OVERRIDE_UNSET.setting) - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue() - // Store System Property if not present - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_UNSET.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun isEnabled_noSystemProperty_overrideUnset_featureFlagOff_returnsFalseAndStoresPropertyUnset() { - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY) - setOverride(OVERRIDE_UNSET.setting) - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse() - // Store System Property if not present - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_UNSET.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - @Suppress("ktlint:standard:max-line-length") - fun isEnabled_systemPropertyNotInteger_overrideOff_featureFlagOn_returnsFalseAndStoresPropertyOff() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc") - setOverride(OVERRIDE_OFF.setting) - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse() - // Store System Property if currently invalid - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_OFF.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - @Suppress("ktlint:standard:max-line-length") - fun isEnabled_systemPropertyInvalidInteger_overrideOff_featureFlagOn_returnsFalseAndStoresPropertyOff() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2") - setOverride(OVERRIDE_OFF.setting) - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse() - // Store System Property if currently invalid - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_OFF.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun isEnabled_systemPropertyOff_overrideOn_featureFlagOn_returnsFalseAndDoesNotUpdateProperty() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, OVERRIDE_OFF.setting.toString()) - setOverride(OVERRIDE_ON.setting) - - // Have a consistent override until reboot - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse() - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_OFF.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun isEnabled_systemPropertyOn_overrideOff_featureFlagOff_returnsTrueAndDoesNotUpdateProperty() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, OVERRIDE_ON.setting.toString()) - setOverride(OVERRIDE_OFF.setting) - - // Have a consistent override until reboot - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue() - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_ON.setting.toString()) - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - @Suppress("ktlint:standard:max-line-length") - fun isEnabled_systemPropertyUnset_overrideOff_featureFlagOn_returnsTrueAndDoesNotUpdateProperty() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, OVERRIDE_UNSET.setting.toString()) - setOverride(OVERRIDE_OFF.setting) - - // Have a consistent override until reboot - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue() - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(OVERRIDE_UNSET.setting.toString()) - } - - @Test @EnableFlags( FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE, @@ -445,12 +343,5 @@ class DesktopModeFlagsTest : ShellTestCase() { DesktopModeFlags::class.java.getDeclaredField("cachedToggleOverride") cachedToggleOverride.isAccessible = true cachedToggleOverride.set(null, null) - - // Clear override cache stored in System property - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY) - } - - private companion object { - const val SYSTEM_PROPERTY_OVERRIDE_KEY = "sys.wmshell.desktopmode.dev_toggle_override" } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 5b95b1588814..1c5d5e963156 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -50,7 +50,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Bundle; -import android.os.IBinder; +import android.os.Handler; import android.window.IWindowContainerToken; import android.window.WindowContainerToken; @@ -104,6 +104,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock SyncTransactionQueue mSyncQueue; @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer; @Mock ShellExecutor mMainExecutor; + @Mock Handler mMainHandler; @Mock DisplayController mDisplayController; @Mock DisplayImeController mDisplayImeController; @Mock DisplayInsetsController mDisplayInsetsController; @@ -134,7 +135,7 @@ public class SplitScreenControllerTests extends ShellTestCase { mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, mIconProvider, Optional.of(mRecentTasks), mLaunchAdjacentController, Optional.of(mWindowDecorViewModel), Optional.of(mDesktopTasksController), - mStageCoordinator, mMultiInstanceHelper, mMainExecutor)); + mStageCoordinator, mMultiInstanceHelper, mMainExecutor, mMainHandler)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index a3009a55198f..29d3fb4cc04e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import android.app.ActivityManager; import android.content.Context; import android.graphics.Rect; +import android.os.Handler; import android.view.SurfaceControl; import android.view.SurfaceSession; @@ -77,13 +78,13 @@ public class SplitTestUtils { DisplayController displayController, DisplayImeController imeController, DisplayInsetsController insetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, + ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, - transitions, transactionPool, mainExecutor, recentTasks, + transitions, transactionPool, mainExecutor, mainHandler, recentTasks, launchAdjacentController, windowDecorViewModel); // Prepare root task for testing. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 34b2eebb15a1..22b408cda0af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -49,9 +49,12 @@ import static org.mockito.Mockito.verify; import android.annotation.NonNull; import android.app.ActivityManager; +import android.os.Handler; import android.os.IBinder; +import android.os.RemoteException; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.IRemoteTransition; import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -107,6 +110,7 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private IconProvider mIconProvider; @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private ShellExecutor mMainExecutor; + @Mock private Handler mMainHandler; @Mock private LaunchAdjacentController mLaunchAdjacentController; @Mock private DefaultMixedHandler mMixedHandler; @Mock private SplitScreen.SplitInvocationListener mInvocationListener; @@ -140,7 +144,7 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mMainExecutor, Optional.empty(), + mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, Optional.empty()); mStageCoordinator.setMixedHandler(mMixedHandler); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); @@ -326,6 +330,32 @@ public class SplitTransitionTests extends ShellTestCase { @Test @UiThreadTest + public void testRemotePassThroughInvoked() throws RemoteException { + RemoteTransition remoteWrapper = mock(RemoteTransition.class); + IRemoteTransition remoteTransition = mock(IRemoteTransition.class); + IBinder remoteBinder = mock(IBinder.class); + doReturn(remoteBinder).when(remoteTransition).asBinder(); + doReturn(remoteTransition).when(remoteWrapper).getRemoteTransition(); + + TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_CHANGE, null, + remoteWrapper); + IBinder transition = mock(IBinder.class); + mMainStage.activate(new WindowContainerTransaction(), false); + mStageCoordinator.handleRequest(transition, request); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .build(); + boolean accepted = mStageCoordinator.startAnimation(transition, info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + mock(Transitions.TransitionFinishCallback.class)); + assertTrue(accepted); + + verify(remoteTransition, times(1)).startAnimation(any(), + any(), any(), any()); + } + + @Test + @UiThreadTest public void testEnterRecentsAndRestore() { enterSplit(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index d18fec2f24ad..eaef704b7d78 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -138,7 +138,8 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, - mMainExecutor, Optional.empty(), mLaunchAdjacentController, Optional.empty())); + mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, + Optional.empty())); mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build(); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); @@ -347,8 +348,7 @@ public class StageCoordinatorTests extends ShellTestCase { assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token); assertThat(options.getPendingIntentBackgroundActivityStartMode()) - .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue(); + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); } @Test @@ -359,10 +359,11 @@ public class StageCoordinatorTests extends ShellTestCase { mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().setVisible(true).build(); when(mStageCoordinator.isSplitActive()).thenReturn(true); when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true); + when(mStageCoordinator.willSleepOnFold()).thenReturn(true); mStageCoordinator.onFoldedStateChanged(true); - assertEquals(mStageCoordinator.mTopStageAfterFoldDismiss, STAGE_TYPE_MAIN); + assertEquals(mStageCoordinator.mLastActiveStage, STAGE_TYPE_MAIN); mStageCoordinator.onFinishedWakingUp(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index b1803e97b107..aeae0bebb697 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -30,6 +30,7 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager import android.os.Handler +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule @@ -62,6 +63,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout @@ -71,6 +73,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.KeyguardChangeListener import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -78,8 +81,6 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener -import java.util.Optional -import java.util.function.Supplier import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -99,6 +100,8 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.spy import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.Optional +import java.util.function.Supplier /** * Tests of [DesktopModeWindowDecorViewModel] @@ -122,6 +125,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockMainChoreographer: Choreographer @Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockSplitScreenController: SplitScreenController @Mock private lateinit var mockDisplayLayout: DisplayLayout @Mock private lateinit var displayInsetsController: DisplayInsetsController @Mock private lateinit var mockSyncQueue: SyncTransactionQueue @@ -136,6 +140,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @Mock private lateinit var mockWindowManager: IWindowManager @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + private val bgExecutor = TestShellExecutor() private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() @@ -155,6 +160,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockShellExecutor, mockMainHandler, mockMainChoreographer, + bgExecutor, shellInit, mockShellCommandHandler, mockWindowManager, @@ -171,7 +177,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockRootTaskDisplayAreaOrganizer, windowDecorByTaskIdSpy, mockInteractionJankMonitor ) - + desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS) whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor) @@ -204,10 +210,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory).create( mContext, mockDisplayController, + mockSplitScreenController, mockTaskOrganizer, task, taskSurface, mockMainHandler, + bgExecutor, mockMainChoreographer, mockSyncQueue, mockRootTaskDisplayAreaOrganizer @@ -228,10 +236,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory, never()).create( mContext, mockDisplayController, + mockSplitScreenController, mockTaskOrganizer, task, taskSurface, mockMainHandler, + bgExecutor, mockMainChoreographer, mockSyncQueue, mockRootTaskDisplayAreaOrganizer @@ -243,10 +253,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory, times(1)).create( mContext, mockDisplayController, + mockSplitScreenController, mockTaskOrganizer, task, taskSurface, mockMainHandler, + bgExecutor, mockMainChoreographer, mockSyncQueue, mockRootTaskDisplayAreaOrganizer @@ -254,6 +266,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testCreateAndDisposeEventReceiver() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) setUpMockDecorationForTask(task) @@ -266,6 +279,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testEventReceiversOnMultipleDisplays() { val secondaryDisplay = createVirtualDisplay() ?: return val secondaryDisplayId = secondaryDisplay.display.displayId @@ -344,7 +358,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskChanging(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } @Test @@ -365,7 +380,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), + any(), any()) } finally { mockitoSession.finishMocking() } @@ -382,7 +398,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } @Test @@ -399,7 +416,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), + any(), any()) } @Test @@ -496,7 +514,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } finally { mockitoSession.finishMocking() } @@ -520,7 +539,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } finally { mockitoSession.finishMocking() } @@ -543,7 +563,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } finally { mockitoSession.finishMocking() } @@ -682,7 +703,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val decoration = mock(DesktopModeWindowDecoration::class.java) whenever( mockDesktopModeWindowDecorFactory.create( - any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index d8606093ac5c..412fef30d4fb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -82,9 +82,12 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; @@ -127,6 +130,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private DisplayController mMockDisplayController; @Mock + private SplitScreenController mMockSplitScreenController; + @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock private Choreographer mMockChoreographer; @@ -165,6 +170,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private SurfaceControl.Transaction mMockTransaction; private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; + private ShellExecutor mBgExecutor = new TestShellExecutor(); /** Set up run before test class. */ @BeforeClass @@ -392,6 +398,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_fullscreenTask_appliesTransactionImmediately() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -418,6 +425,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -429,6 +437,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_fullscreenTask_postsViewHostCreation() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -457,6 +466,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_removesExistingHandlerCallback() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -471,6 +481,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void close_removesExistingHandlerCallback() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -656,8 +667,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ActivityManager.RunningTaskInfo taskInfo, MaximizeMenuFactory maximizeMenuFactory) { final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, - mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, - mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, + mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer, + taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer, + mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, mMockSurfaceControlViewHostFactory, maximizeMenuFactory); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt index 3fbab0f9e2bb..56224b4b4025 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt @@ -85,6 +85,23 @@ class DragDetectorTest { } @Test + fun testNoMove_mouse_passesDownAndUp() { + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_UP, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + } + + @Test fun testMoveInSlop_touch_passesDownAndUp() { `when`(eventHandler.handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_DOWN @@ -166,6 +183,52 @@ class DragDetectorTest { } @Test + fun testDownMoveDown_shouldIgnoreTheSecondDownMotion() { + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + val newX = X + SLOP + 1 + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + } + + @Test + fun testDownMouseMoveDownTouch_shouldIgnoreTheTouchDownMotion() { + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + val newX = X + SLOP + 1 + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + } + + @Test fun testPassesHoverEnter() { `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_HOVER_ENTER diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index adda9a688172..e548f8f1eb2a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -130,7 +130,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testFullscreenMenuUsesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) - val handleMenu = createAndShowHandleMenu() + val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of display. @@ -142,7 +142,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testFreeformMenu_usesViewHostViewContainer() { createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) - handleMenu = createAndShowHandleMenu() + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer) // Verify menu is created near top-left of task. val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN) @@ -153,7 +153,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testSplitLeftMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) - handleMenu = createAndShowHandleMenu() + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of split left task. @@ -168,7 +168,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testSplitRightMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) - handleMenu = createAndShowHandleMenu() + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of split right task. @@ -208,16 +208,30 @@ class HandleMenuTest : ShellTestCase() { } } - private fun createAndShowHandleMenu(): HandleMenu { + private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu { val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { R.layout.desktop_mode_app_header } else { R.layout.desktop_mode_app_handle } + val captionX = when (mockDesktopWindowDecoration.mTaskInfo.windowingMode) { + WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + WINDOWING_MODE_FREEFORM -> 0 + WINDOWING_MODE_MULTI_WINDOW -> { + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + } else { + (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + } + } + else -> error("Invalid windowing mode") + } val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, onClickListener, onTouchListener, appIcon, appName, displayController, - splitScreenController, true /* shouldShowWindowingPill */, - true /* shouldShowBrowserPill */, 50 /* captionHeight */) + splitScreenController, shouldShowWindowingPill = true, + shouldShowBrowserPill = true, captionWidth = HANDLE_WIDTH, captionHeight = 50, + captionX = captionX + ) handleMenu.show() return handleMenu } @@ -233,5 +247,6 @@ class HandleMenuTest : ShellTestCase() { private const val MENU_START_MARGIN = 20 private const val MENU_PILL_ELEVATION = 2 private const val MENU_PILL_SPACING_MARGIN = 4 + private const val HANDLE_WIDTH = 80 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt index d3e996b12e1f..3b490550ab61 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt @@ -68,12 +68,12 @@ class AdditionalSystemViewContainerTest : ShellTestCase() { fun testReleaseView_ViewRemoved() { viewContainer = AdditionalSystemViewContainer( mockContext, - R.layout.desktop_mode_window_decor_handle_menu, TASK_ID, X, Y, WIDTH, - HEIGHT + HEIGHT, + R.layout.desktop_mode_window_decor_handle_menu ) verify(mockWindowManager).addView(eq(mockView), any()) viewContainer.releaseView() diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index ad963dd913cf..93118aeafaaf 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -40,6 +40,7 @@ namespace android { namespace uirenderer { +std::mutex TestUtils::sMutex; std::unordered_map<int, TestUtils::CallCounts> TestUtils::sMockFunctorCounts{}; SkColor TestUtils::interpolateColor(float fraction, SkColor start, SkColor end) { diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 0ede902b1b95..8ab2b16601c3 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -305,22 +305,26 @@ public: .onSync = [](int functor, void* client_data, const WebViewSyncData& data) { expectOnRenderThread("onSync"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].sync++; }, .onContextDestroyed = [](int functor, void* client_data) { expectOnRenderThread("onContextDestroyed"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].contextDestroyed++; }, .onDestroyed = [](int functor, void* client_data) { expectOnRenderThread("onDestroyed"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].destroyed++; }, .removeOverlays = [](int functor, void* data, void (*mergeTransaction)(ASurfaceTransaction*)) { expectOnRenderThread("removeOverlays"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].removeOverlays++; }, }; @@ -329,6 +333,7 @@ public: callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params, const WebViewOverlayData& overlay_params) { expectOnRenderThread("draw"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].glesDraw++; }; break; @@ -336,15 +341,18 @@ public: callbacks.vk.initialize = [](int functor, void* data, const VkFunctorInitParams& params) { expectOnRenderThread("initialize"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].vkInitialize++; }; callbacks.vk.draw = [](int functor, void* data, const VkFunctorDrawParams& params, const WebViewOverlayData& overlayParams) { expectOnRenderThread("draw"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].vkDraw++; }; callbacks.vk.postDraw = [](int functor, void* data) { expectOnRenderThread("postDraw"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].vkPostDraw++; }; break; @@ -352,11 +360,16 @@ public: return callbacks; } - static CallCounts& countsForFunctor(int functor) { return sMockFunctorCounts[functor]; } + static CallCounts copyCountsForFunctor(int functor) { + std::scoped_lock lock(sMutex); + return sMockFunctorCounts[functor]; + } static SkFont defaultFont(); private: + // guards sMockFunctorCounts + static std::mutex sMutex; static std::unordered_map<int, CallCounts> sMockFunctorCounts; static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) { diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index e727ea899098..690a60a470bc 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -239,19 +239,21 @@ TEST(RenderNode, releasedCallback) { TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); }); - auto& counts = TestUtils::countsForFunctor(functor); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(0, counts.destroyed); TestUtils::recordNode(*node, [&](Canvas& canvas) { canvas.drawWebViewFunctor(functor); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(0, counts.destroyed); TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(0, counts.destroyed); @@ -265,6 +267,7 @@ TEST(RenderNode, releasedCallback) { }); // Fence on any remaining post'd work TestUtils::runOnRenderThreadUnmanaged([] (RenderThread&) {}); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(1, counts.destroyed); } diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index 064d42ec8941..26b47290d149 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -101,7 +101,7 @@ TEST(SkiaDisplayList, syncContexts) { SkCanvas dummyCanvas; int functor1 = TestUtils::createMockFunctor(); - auto& counts = TestUtils::countsForFunctor(functor1); + auto counts = TestUtils::copyCountsForFunctor(functor1); skiaDL.mChildFunctors.push_back( skiaDL.allocateDrawable<GLFunctorDrawable>(functor1, &dummyCanvas)); WebViewFunctor_release(functor1); @@ -118,6 +118,7 @@ TEST(SkiaDisplayList, syncContexts) { }); }); + counts = TestUtils::copyCountsForFunctor(functor1); EXPECT_EQ(counts.sync, 1); EXPECT_EQ(counts.destroyed, 0); EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds); @@ -126,6 +127,7 @@ TEST(SkiaDisplayList, syncContexts) { TestUtils::runOnRenderThread([](auto&) { // Fence }); + counts = TestUtils::copyCountsForFunctor(functor1); EXPECT_EQ(counts.destroyed, 1); } diff --git a/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp b/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp index 5e8f13d261c7..09ce98a2e53d 100644 --- a/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp +++ b/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp @@ -40,7 +40,7 @@ TEST(WebViewFunctor, createDestroyGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // Empty, don't care }); - auto& counts = TestUtils::countsForFunctor(functor); + auto counts = TestUtils::copyCountsForFunctor(functor); // We never initialized, so contextDestroyed == 0 EXPECT_EQ(0, counts.contextDestroyed); EXPECT_EQ(1, counts.destroyed); @@ -59,7 +59,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // fence }); - auto& counts = TestUtils::countsForFunctor(functor); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(0, counts.sync); EXPECT_EQ(0, counts.contextDestroyed); EXPECT_EQ(0, counts.destroyed); @@ -69,6 +69,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { handle->sync(syncData); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); TestUtils::runOnRenderThreadUnmanaged([&](auto&) { @@ -76,6 +77,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { handle->sync(syncData); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); handle.clear(); @@ -84,6 +86,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { // fence }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(0, counts.contextDestroyed); EXPECT_EQ(1, counts.destroyed); @@ -98,7 +101,6 @@ TEST(WebViewFunctor, createSyncDrawGLES) { auto handle = WebViewFunctorManager::instance().handleFor(functor); ASSERT_TRUE(handle); WebViewFunctor_release(functor); - auto& counts = TestUtils::countsForFunctor(functor); for (int i = 0; i < 5; i++) { TestUtils::runOnRenderThreadUnmanaged([&](auto&) { WebViewSyncData syncData; @@ -112,6 +114,7 @@ TEST(WebViewFunctor, createSyncDrawGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // fence }); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(5, counts.sync); EXPECT_EQ(10, counts.glesDraw); EXPECT_EQ(1, counts.contextDestroyed); @@ -127,13 +130,13 @@ TEST(WebViewFunctor, contextDestroyedGLES) { auto handle = WebViewFunctorManager::instance().handleFor(functor); ASSERT_TRUE(handle); WebViewFunctor_release(functor); - auto& counts = TestUtils::countsForFunctor(functor); TestUtils::runOnRenderThreadUnmanaged([&](auto&) { WebViewSyncData syncData; handle->sync(syncData); DrawGlInfo drawInfo; handle->drawGl(drawInfo); }); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(1, counts.glesDraw); EXPECT_EQ(0, counts.contextDestroyed); @@ -141,6 +144,7 @@ TEST(WebViewFunctor, contextDestroyedGLES) { TestUtils::runOnRenderThreadUnmanaged([](auto& rt) { rt.destroyRenderingContext(); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(1, counts.glesDraw); EXPECT_EQ(1, counts.contextDestroyed); @@ -151,6 +155,7 @@ TEST(WebViewFunctor, contextDestroyedGLES) { DrawGlInfo drawInfo; handle->drawGl(drawInfo); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(2, counts.glesDraw); EXPECT_EQ(1, counts.contextDestroyed); @@ -159,6 +164,7 @@ TEST(WebViewFunctor, contextDestroyedGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // fence }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(2, counts.glesDraw); EXPECT_EQ(2, counts.contextDestroyed); diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index acfe473c260c..0edaaefbd75d 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -2,6 +2,20 @@ package: "android.location.flags" container: "system" flag { + name: "keep_gnss_stationary_throttling" + namespace: "location" + description: "Keeps stationary throttling for the GNSS provider even if the disable_stationary_throttling flag is true." + bug: "354000147" +} + +flag { + name: "disable_stationary_throttling" + namespace: "location" + description: "Disables stationary throttling for all providers" + bug: "354000147" +} + +flag { name: "new_geocoder" namespace: "location" description: "Flag for new Geocoder APIs" diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 2d0e7abbe890..a255f730b0f3 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2651,4 +2651,11 @@ public class AudioSystem * @hide */ public static native boolean isBluetoothVariableLatencyEnabled(); + + /** + * Register a native listener for system property sysprop + * @param callback the listener which fires when the property changes + * @hide + */ + public static native void listenForSystemPropertyChange(String sysprop, Runnable callback); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 999f40e53952..1c5049e891e9 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -23,6 +23,7 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.Context; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Build; @@ -140,6 +141,7 @@ public final class MediaProjection { /** * @hide */ + @Nullable public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, boolean isSecure, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { @@ -192,6 +194,11 @@ public final class MediaProjection { * <li>If attempting to create a new virtual display * associated with this MediaProjection instance after it has * been stopped by invoking {@link #stop()}. + * <li>If attempting to create a new virtual display + * associated with this MediaProjection instance after a + * {@link MediaProjection.Callback#onStop()} callback has been + * received due to the user or the system stopping the + * MediaProjection session. * <li>If the target SDK is {@link * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, * and if this instance has already taken a recording through @@ -208,12 +215,17 @@ public final class MediaProjection { * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}. * Instead, recording doesn't begin until the user re-grants * consent in the dialog. + * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay} + * could be created. * @see VirtualDisplay * @see VirtualDisplay.Callback */ + @SuppressWarnings("RequiresPermission") + @Nullable public VirtualDisplay createVirtualDisplay(@NonNull String name, - int width, int height, int dpi, int flags, @Nullable Surface surface, - @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { + int width, int height, int dpi, @VirtualDisplayFlag int flags, + @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, + @Nullable Handler handler) { if (shouldMediaProjectionRequireCallback()) { if (mCallbacks.isEmpty()) { final IllegalStateException e = new IllegalStateException( diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 88770d487a83..186b69b4107b 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -912,8 +912,10 @@ class InstallRepository(private val context: Context) { "message: $message" ) } + + val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) + if (statusCode == PackageInstaller.STATUS_SUCCESS) { - val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) val resultIntent = if (shouldReturnResult) { Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED) } else { @@ -922,12 +924,34 @@ class InstallRepository(private val context: Context) { } _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) } else { - if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) { + // TODO (b/346655018): Use INSTALL_FAILED_ABORTED legacyCode in the condition + // statusCode can be STATUS_FAILURE_ABORTED if: + // 1. GPP blocks an install. + // 2. User denies ownership update explicitly. + // InstallFailed dialog must not be shown only when the user denies ownership update. We + // must show this dialog for all other install failures. + + val userDenied = + statusCode == PackageInstaller.STATUS_FAILURE_ABORTED && + legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_TIMEOUT && + legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE + + if (shouldReturnResult) { + val resultIntent = Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus) _installResult.setValue( - InstallFailed(appSnippet, statusCode, legacyStatus, message) + InstallFailed( + legacyCode = legacyStatus, + statusCode = statusCode, + shouldReturnResult = true, + resultIntent = resultIntent + ) ) - } else { + } else if (userDenied) { _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR)) + } else { + _installResult.setValue( + InstallFailed(appSnippet, legacyStatus, statusCode, message) + ) } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt index 5dd4d2905f47..8de8fbb3e688 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -19,6 +19,7 @@ package com.android.packageinstaller.v2.model import android.app.Activity import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.PackageInstaller import android.graphics.drawable.Drawable sealed class InstallStage(val stageCode: Int) { @@ -77,11 +78,10 @@ data class InstallSuccess( val shouldReturnResult: Boolean = false, /** * - * * If the caller is requesting a result back, this will hold the Intent with - * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent - * back to the caller. + * * If the caller is requesting a result back, this will hold an Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED]. * - * * If the caller doesn't want the result back, this will hold the Intent that launches + * * If the caller doesn't want the result back, this will hold an Intent that launches * the newly installed / updated app if a launchable activity exists. */ val resultIntent: Intent? = null, @@ -95,17 +95,23 @@ data class InstallSuccess( } data class InstallFailed( - private val appSnippet: PackageUtil.AppSnippet, + private val appSnippet: PackageUtil.AppSnippet? = null, val legacyCode: Int, val statusCode: Int, - val message: String?, + val message: String? = null, + val shouldReturnResult: Boolean = false, + /** + * If the caller is requesting a result back, this will hold an Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to the [PackageInstaller.EXTRA_LEGACY_STATUS]. + */ + val resultIntent: Intent? = null ) : InstallStage(STAGE_FAILED) { val appIcon: Drawable? - get() = appSnippet.icon + get() = appSnippet?.icon val appLabel: String? - get() = appSnippet.label as String? + get() = appSnippet?.label as String? } data class InstallAborted( diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt index 31b9ccbdb838..e2ab31662380 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -171,15 +171,20 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val successIntent = success.resultIntent setResult(Activity.RESULT_OK, successIntent, true) } else { - val successFragment = InstallSuccessFragment(success) - showDialogInner(successFragment) + val successDialog = InstallSuccessFragment(success) + showDialogInner(successDialog) } } InstallStage.STAGE_FAILED -> { val failed = installStage as InstallFailed - val failedDialog = InstallFailedFragment(failed) - showDialogInner(failedDialog) + if (failed.shouldReturnResult) { + val failureIntent = failed.resultIntent + setResult(Activity.RESULT_FIRST_USER, failureIntent, true) + } else { + val failureDialog = InstallFailedFragment(failed) + showDialogInner(failureDialog) + } } else -> { diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index d6345cec7c30..f36344aa4846 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath allprojects { extra["androidTop"] = androidTop - extra["jetpackComposeVersion"] = "1.7.0-beta02" + extra["jetpackComposeVersion"] = "1.7.0-beta05" } subprojects { diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index a842009a09bb..1cca73af9a42 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,7 +15,7 @@ # [versions] -agp = "8.5.0" +agp = "8.5.1" compose-compiler = "1.5.11" dexmaker-mockito = "2.28.3" jvm = "17" diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.8-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip Binary files differindex 77e6ad3f2117..9a97e4674448 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.8-bin.zip +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar Binary files differindex e6441136f3d4..2c3521197d7c 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties index 91d2a3ab217d..9f29c77d55f6 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties @@ -16,6 +16,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=gradle-8.8-bin.zip +distributionUrl=gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/SettingsLib/Spa/gradlew b/packages/SettingsLib/Spa/gradlew index b740cf13397a..f5feea6d6b11 100755 --- a/packages/SettingsLib/Spa/gradlew +++ b/packages/SettingsLib/Spa/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 6df0e9956f27..ac44a1be4cff 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -25,9 +25,6 @@ android_library { use_resource_processor: true, static_libs: [ "SettingsLibColor", - "androidx.slice_slice-builders", - "androidx.slice_slice-core", - "androidx.slice_slice-view", "androidx.compose.animation_animation", "androidx.compose.material3_material3", "androidx.compose.material_material-icons-extended", diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index 9b8ecf773661..ce3d96e2d388 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -53,17 +53,14 @@ android { dependencies { api(project(":SettingsLibColor")) - api("androidx.appcompat:appcompat:1.7.0-rc01") - api("androidx.slice:slice-builders:1.1.0-alpha02") - api("androidx.slice:slice-core:1.1.0-alpha02") - api("androidx.slice:slice-view:1.1.0-alpha02") - api("androidx.compose.material3:material3:1.3.0-beta02") + api("androidx.appcompat:appcompat:1.7.0") + api("androidx.compose.material3:material3:1.3.0-beta04") api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion") api("androidx.lifecycle:lifecycle-livedata-ktx") api("androidx.lifecycle:lifecycle-runtime-compose") - api("androidx.navigation:navigation-compose:2.8.0-beta02") + api("androidx.navigation:navigation-compose:2.8.0-beta05") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.11.0") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") @@ -97,10 +94,6 @@ tasks.register<JacocoReport>("coverageReport") { // Excludes debug functions "com/android/settingslib/spa/framework/compose/TimeMeasurer*", - - // Excludes slice demo presenter & provider - "com/android/settingslib/spa/slice/presenter/Demo*", - "com/android/settingslib/spa/slice/provider/Demo*", ) ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt index 085c3c6e026b..ddb571d1392f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt @@ -55,7 +55,6 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings toPage = toPage, // attributes - // TODO: set isEnabled & (isAllowSearch, hasSliceSupport) separately isAllowSearch = isEnabled && isAllowSearch, isSearchDataDynamic = isSearchDataDynamic, hasMutableStatus = hasMutableStatus, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt index 95c7d2371b3d..cc5351a3b81d 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt @@ -57,8 +57,8 @@ interface SettingsPageProvider { /** * The API to indicate whether the page is enabled or not. * During SPA page migration, one can use it to enable certain pages in one release. - * When the page is disabled, all its related functionalities, such as browsing, search, - * slice provider, are disabled as well. + * When the page is disabled, all its related functionalities, such as browsing and search, + * are disabled as well. */ fun isEnabled(arguments: Bundle?): Boolean = true diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt index d8c35a36d061..9e8ca0cd71a4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt @@ -25,7 +25,6 @@ import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory const val SESSION_UNKNOWN = "unknown" const val SESSION_BROWSE = "browse" const val SESSION_SEARCH = "search" -const val SESSION_SLICE = "slice" const val SESSION_EXTERNAL = "external" const val KEY_DESTINATION = "spaActivityDestination" diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 4028b73a2c71..714f9519f378 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -18,9 +18,7 @@ package com.android.settingslib.mobile.dataservice; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; -import android.telephony.UiccCardInfo; import android.telephony.UiccPortInfo; -import android.telephony.UiccSlotInfo; import android.telephony.UiccSlotMapping; public class DataServiceUtils { @@ -71,53 +69,9 @@ public class DataServiceUtils { public static final String COLUMN_ID = "sudId"; /** - * The name of the physical slot index column, see - * {@link UiccSlotMapping#getPhysicalSlotIndex()}. - */ - public static final String COLUMN_PHYSICAL_SLOT_INDEX = "physicalSlotIndex"; - - /** - * The name of the logical slot index column, see - * {@link UiccSlotMapping#getLogicalSlotIndex()}. - */ - public static final String COLUMN_LOGICAL_SLOT_INDEX = "logicalSlotIndex"; - - /** - * The name of the card ID column, see {@link UiccCardInfo#getCardId()}. - */ - public static final String COLUMN_CARD_ID = "cardId"; - - /** - * The name of the eUICC state column, see {@link UiccCardInfo#isEuicc()}. - */ - public static final String COLUMN_IS_EUICC = "isEuicc"; - - /** - * The name of the multiple enabled profiles supported state column, see - * {@link UiccCardInfo#isMultipleEnabledProfilesSupported()}. - */ - public static final String COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED = - "isMultipleEnabledProfilesSupported"; - - /** - * The name of the card state column, see {@link UiccSlotInfo#getCardStateInfo()}. - */ - public static final String COLUMN_CARD_STATE = "cardState"; - - /** - * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}. - */ - public static final String COLUMN_IS_REMOVABLE = "isRemovable"; - - /** * The name of the active state column, see {@link UiccPortInfo#isActive()}. */ public static final String COLUMN_IS_ACTIVE = "isActive"; - - /** - * The name of the port index column, see {@link UiccPortInfo#getPortIndex()}. - */ - public static final String COLUMN_PORT_INDEX = "portIndex"; } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java index c92204fa1f39..5f7fa278082b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java @@ -19,14 +19,13 @@ package com.android.settingslib.mobile.dataservice; import android.content.Context; import android.util.Log; -import java.util.List; -import java.util.Objects; - import androidx.lifecycle.LiveData; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; -import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.List; +import java.util.Objects; @Database(entities = {SubscriptionInfoEntity.class, UiccInfoEntity.class, MobileNetworkInfoEntity.class}, exportSchema = false, version = 1) @@ -132,13 +131,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { } /** - * Query the UICC info by the subscription ID from the UiccInfoEntity table. - */ - public LiveData<UiccInfoEntity> queryUiccInfoById(String id) { - return mUiccInfoDao().queryUiccInfoById(id); - } - - /** * Delete the subscriptionInfo info by the subscription ID from the SubscriptionInfoEntity * table. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java index 7e60421d0ab4..90e5189fdf1d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java @@ -16,14 +16,14 @@ package com.android.settingslib.mobile.dataservice; -import java.util.List; - import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; +import java.util.List; + @Dao public interface UiccInfoDao { @@ -34,14 +34,6 @@ public interface UiccInfoDao { + DataServiceUtils.UiccInfoData.COLUMN_ID) LiveData<List<UiccInfoEntity>> queryAllUiccInfos(); - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_ID + " = :subId") - LiveData<UiccInfoEntity> queryUiccInfoById(String subId); - - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC + " = :isEuicc") - LiveData<List<UiccInfoEntity>> queryUiccInfosByEuicc(boolean isEuicc); - @Query("SELECT COUNT(*) FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME) int count(); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java index 2ccf295007dc..0f80edf52d80 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java @@ -26,20 +26,9 @@ import androidx.room.PrimaryKey; @Entity(tableName = DataServiceUtils.UiccInfoData.TABLE_NAME) public class UiccInfoEntity { - public UiccInfoEntity(@NonNull String subId, @NonNull String physicalSlotIndex, - int logicalSlotIndex, int cardId, boolean isEuicc, - boolean isMultipleEnabledProfilesSupported, int cardState, boolean isRemovable, - boolean isActive, int portIndex) { + public UiccInfoEntity(@NonNull String subId, boolean isActive) { this.subId = subId; - this.physicalSlotIndex = physicalSlotIndex; - this.logicalSlotIndex = logicalSlotIndex; - this.cardId = cardId; - this.isEuicc = isEuicc; - this.isMultipleEnabledProfilesSupported = isMultipleEnabledProfilesSupported; - this.cardState = cardState; - this.isRemovable = isRemovable; this.isActive = isActive; - this.portIndex = portIndex; } @PrimaryKey @@ -47,48 +36,14 @@ public class UiccInfoEntity { @NonNull public String subId; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PHYSICAL_SLOT_INDEX) - @NonNull - public String physicalSlotIndex; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_LOGICAL_SLOT_INDEX) - public int logicalSlotIndex; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_ID) - public int cardId; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC) - public boolean isEuicc; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED) - public boolean isMultipleEnabledProfilesSupported; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_STATE) - public int cardState; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_REMOVABLE) - public boolean isRemovable; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_ACTIVE) public boolean isActive; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PORT_INDEX) - public int portIndex; - - @Override public int hashCode() { int result = 17; result = 31 * result + subId.hashCode(); - result = 31 * result + physicalSlotIndex.hashCode(); - result = 31 * result + logicalSlotIndex; - result = 31 * result + cardId; - result = 31 * result + Boolean.hashCode(isEuicc); - result = 31 * result + Boolean.hashCode(isMultipleEnabledProfilesSupported); - result = 31 * result + cardState; - result = 31 * result + Boolean.hashCode(isRemovable); result = 31 * result + Boolean.hashCode(isActive); - result = 31 * result + portIndex; return result; } @@ -102,40 +57,15 @@ public class UiccInfoEntity { } UiccInfoEntity info = (UiccInfoEntity) obj; - return TextUtils.equals(subId, info.subId) - && TextUtils.equals(physicalSlotIndex, info.physicalSlotIndex) - && logicalSlotIndex == info.logicalSlotIndex - && cardId == info.cardId - && isEuicc == info.isEuicc - && isMultipleEnabledProfilesSupported == info.isMultipleEnabledProfilesSupported - && cardState == info.cardState - && isRemovable == info.isRemovable - && isActive == info.isActive - && portIndex == info.portIndex; + return TextUtils.equals(subId, info.subId) && isActive == info.isActive; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(" {UiccInfoEntity(subId = ") .append(subId) - .append(", logicalSlotIndex = ") - .append(physicalSlotIndex) - .append(", logicalSlotIndex = ") - .append(logicalSlotIndex) - .append(", cardId = ") - .append(cardId) - .append(", isEuicc = ") - .append(isEuicc) - .append(", isMultipleEnabledProfilesSupported = ") - .append(isMultipleEnabledProfilesSupported) - .append(", cardState = ") - .append(cardState) - .append(", isRemovable = ") - .append(isRemovable) .append(", isActive = ") .append(isActive) - .append(", portIndex = ") - .append(portIndex) .append(")}"); return builder.toString(); } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java index 1e75014d2017..3906749ae494 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java @@ -24,9 +24,9 @@ import android.os.UserHandle; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; @@ -109,7 +109,7 @@ public class InputMethodPreferenceTest { final boolean systemIme, final String name) { return new InputMethodPreference( - InstrumentationRegistry.getTargetContext(), + InstrumentationRegistry.getInstrumentation().getTargetContext(), createInputMethodInfo(systemIme, name), title, true /* isAllowedByOrganization */, @@ -119,7 +119,8 @@ public class InputMethodPreferenceTest { private static InputMethodInfo createInputMethodInfo( final boolean systemIme, final String name) { - final Context targetContext = InstrumentationRegistry.getTargetContext(); + final Context targetContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); final Locale systemLocale = targetContext .getResources() .getConfiguration() diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java index f1c0beae0dc0..2c3478d6e235 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java @@ -18,9 +18,9 @@ package com.android.settingslib.inputmethod; import android.text.TextUtils; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; @@ -104,7 +104,7 @@ public class InputMethodSubtypePreferenceTest { final Locale subtypeLocale = TextUtils.isEmpty(subtypeLanguageTag) ? null : Locale.forLanguageTag(subtypeLanguageTag); return new InputMethodSubtypePreference( - InstrumentationRegistry.getTargetContext(), + InstrumentationRegistry.getInstrumentation().getTargetContext(), key, subtypeName, subtypeLocale, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 5f236516785d..2b8b23e3dba8 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -282,5 +282,6 @@ public class SecureSettings { Settings.Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, Settings.Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, Settings.Secure.MANDATORY_BIOMETRICS, + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index c8da8afc25c3..cc5302bdd99a 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -441,5 +441,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, NONE_NEGATIVE_LONG_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.MANDATORY_BIOMETRICS, new InclusiveIntegerRangeValidator(0, 1)); + VALIDATORS.put(Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, + new InclusiveIntegerRangeValidator(0, 1)); } } diff --git a/packages/SystemUI/aconfig/communal.aconfig b/packages/SystemUI/aconfig/communal.aconfig index afcd8a9624c8..f2b5efa6a571 100644 --- a/packages/SystemUI/aconfig/communal.aconfig +++ b/packages/SystemUI/aconfig/communal.aconfig @@ -8,12 +8,3 @@ flag { bug: "304584416" } -flag { - name: "enable_widget_picker_size_filter" - namespace: "communal" - description: "Enables passing a size filter to the widget picker" - bug: "345482907" - metadata { - purpose: PURPOSE_BUGFIX - } -} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 035e2fbf8e65..02e042327d66 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -988,6 +988,16 @@ flag { } flag { + name: "communal_scene_ktf_refactor" + namespace: "systemui" + description: "refactors the syncing mechanism between communal STL and KTF state." + bug: "327225415" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "app_clips_backlinks" namespace: "systemui" description: "Enables Backlinks improvement feature in App Clips" @@ -1196,6 +1206,16 @@ flag { } flag { + namespace: "systemui" + name: "remove_update_listener_in_qs_icon_view_impl" + description: "Remove update listeners in QsIconViewImpl class to avoid memory leak." + bug: "327078684" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "sim_pin_race_condition_on_restart" namespace: "systemui" description: "The SIM PIN screen may be shown incorrectly on reboot" @@ -1213,4 +1233,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "lockscreen_preview_renderer_create_on_main_thread" + namespace: "systemui" + description: "Force preview renderer to be created on the main thread" + bug: "343732179" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 1c02d3f7662b..68e968fdfb4e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1004,6 +1004,7 @@ private fun WidgetContent( } .thenIf(viewModel.isEditMode) { Modifier.semantics { + onClick(clickActionLabel, null) contentDescription = accessibilityLabel val deleteAction = CustomAccessibilityAction(removeWidgetActionLabel) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt index 620892adc286..b4c1a2e85daf 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -102,8 +101,6 @@ constructor( val interactionSource = remember { MutableInteractionSource() } val focusRequester = remember { FocusRequester() } - val context = LocalContext.current - LaunchedEffect(Unit) { // Adding a delay to ensure the animation completes before requesting focus delay(250) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt index c34fb38f8558..c9938552a3d5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt @@ -55,7 +55,7 @@ fun LockscreenLongPress( modifier .pointerInput(isEnabled) { if (isEnabled) { - detectLongPressGesture { viewModel.onLongPress() } + detectLongPressGesture { viewModel.onLongPress(isA11yAction = false) } } } .pointerInput(Unit) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt index d629eec1c45a..f8bd633d99a6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt @@ -35,7 +35,7 @@ import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.res.R import com.android.systemui.util.animation.MeasurementInput -private object MediaCarousel { +object MediaCarousel { object Elements { internal val Content = ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt index 039813390f1e..a22bc34381fd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt @@ -25,7 +25,7 @@ import com.android.systemui.scene.shared.model.Scenes /** [ElementScenePicker] implementation for the media carousel object. */ object MediaScenePicker : ElementScenePicker { - private val shadeLockscreenFraction = 0.65f + const val SHADE_FRACTION = 0.66f private val scenes = setOf( Scenes.Lockscreen, @@ -44,7 +44,7 @@ object MediaScenePicker : ElementScenePicker { return when { // TODO: 352052894 - update with the actual scene picking transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> { - if (transition.progress < shadeLockscreenFraction) { + if (transition.progress < SHADE_FRACTION) { Scenes.Lockscreen } else { Scenes.Shade @@ -53,7 +53,7 @@ object MediaScenePicker : ElementScenePicker { // TODO: 345467290 - update with the actual scene picking transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> { - if (transition.progress < 1f - shadeLockscreenFraction) { + if (transition.progress < 1f - SHADE_FRACTION) { Scenes.Shade } else { Scenes.Lockscreen diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt index df47cbab4b90..7d46c7570dac 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt @@ -25,6 +25,8 @@ import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance import com.android.compose.animation.scene.UserActionDistanceScope +import com.android.systemui.media.controls.ui.composable.MediaCarousel +import com.android.systemui.media.controls.ui.composable.MediaScenePicker import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes @@ -59,10 +61,13 @@ fun TransitionBuilder.toShadeTransition( fade(QuickSettings.Elements.SplitShadeQuickSettings) fade(QuickSettings.Elements.FooterActions) } - translate( - QuickSettings.Elements.QuickQuickSettings, - y = -ShadeHeader.Dimensions.CollapsedHeight * .66f - ) + + val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaScenePicker.SHADE_FRACTION + val qsExpansionDiff = + ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight + + translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation) + translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation)) translate(Notifications.Elements.NotificationScrim, Edge.Top, false) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index ea740a8d8de4..82c85d1fa38b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.launch * the currently running transition, if there is one. */ internal fun CoroutineScope.animateToScene( - layoutState: BaseSceneTransitionLayoutState, + layoutState: MutableSceneTransitionLayoutStateImpl, target: SceneKey, transitionKey: TransitionKey?, ): TransitionState.Transition? { @@ -154,7 +154,7 @@ internal fun CoroutineScope.animateToScene( } private fun CoroutineScope.animate( - layoutState: BaseSceneTransitionLayoutState, + layoutState: MutableSceneTransitionLayoutStateImpl, targetScene: SceneKey, transitionKey: TransitionKey?, isInitiatedByUserInput: Boolean, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 78ba7defe77c..5b328b8cdd9c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -52,11 +52,19 @@ internal interface DraggableHandler { * and [onStop] methods. */ internal interface DragController { - /** Drag the current scene by [delta] pixels. */ - fun onDrag(delta: Float) + /** + * Drag the current scene by [delta] pixels. + * + * @return the consumed [delta] + */ + fun onDrag(delta: Float): Float - /** Starts a transition to a target scene. */ - fun onStop(velocity: Float, canChangeScene: Boolean) + /** + * Starts a transition to a target scene. + * + * @return the consumed [velocity] + */ + fun onStop(velocity: Float, canChangeScene: Boolean): Float } internal class DraggableHandlerImpl( @@ -272,8 +280,10 @@ private class DragControllerImpl( * * @return the consumed delta */ - override fun onDrag(delta: Float) { - if (delta == 0f || !isDrivingTransition || swipeTransition.isFinishing) return + override fun onDrag(delta: Float): Float { + if (delta == 0f || !isDrivingTransition || swipeTransition.isFinishing) { + return 0f + } swipeTransition.dragOffset += delta val (fromScene, acceleratedOffset) = @@ -289,7 +299,7 @@ private class DragControllerImpl( if (result == null) { onStop(velocity = delta, canChangeScene = true) - return + return 0f } if ( @@ -314,6 +324,8 @@ private class DragControllerImpl( updateTransition(swipeTransition) } + + return delta } /** @@ -351,10 +363,10 @@ private class DragControllerImpl( } } - override fun onStop(velocity: Float, canChangeScene: Boolean) { + override fun onStop(velocity: Float, canChangeScene: Boolean): Float { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition || swipeTransition.isFinishing) { - return + return 0f } // Important: Make sure that all the code here references the current transition when @@ -370,9 +382,6 @@ private class DragControllerImpl( // immediately go back B => A. if (targetScene != swipeTransition._currentScene) { swipeTransition._currentScene = targetScene - with(draggableHandler.layoutImpl.state) { - draggableHandler.coroutineScope.onChangeScene(targetScene.key) - } } swipeTransition.animateOffset( @@ -443,7 +452,7 @@ private class DragControllerImpl( if (result == null) { // We will not animate swipeTransition.snapToScene(fromScene.key) - return + return 0f } val newSwipeTransition = @@ -465,6 +474,9 @@ private class DragControllerImpl( animateTo(targetScene = fromScene, targetOffset = 0f) } } + + // The onStop animation consumes any remaining velocity. + return velocity } /** @@ -512,7 +524,7 @@ private class DragControllerImpl( } private fun SwipeTransition( - layoutState: BaseSceneTransitionLayoutState, + layoutState: MutableSceneTransitionLayoutStateImpl, coroutineScope: CoroutineScope, fromScene: Scene, result: UserActionResult, @@ -567,7 +579,7 @@ private fun SwipeTransition(old: SwipeTransition): SwipeTransition { private class SwipeTransition( val layoutImpl: SceneTransitionLayoutImpl, - val layoutState: BaseSceneTransitionLayoutState, + val layoutState: MutableSceneTransitionLayoutStateImpl, val coroutineScope: CoroutineScope, override val key: TransitionKey?, val _fromScene: Scene, @@ -1084,17 +1096,13 @@ internal class NestedScrollHandlerImpl( // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is // initiated in a nested child. controller.onDrag(delta = offsetAvailable) - - offsetAvailable }, onStop = { velocityAvailable -> val controller = dragController ?: error("Should be called after onStart") - controller.onStop(velocity = velocityAvailable, canChangeScene = canChangeScene) - - dragController = null - // The onDragStopped animation consumes any remaining velocity. - velocityAvailable + controller + .onStop(velocity = velocityAvailable, canChangeScene = canChangeScene) + .also { dragController = null } }, ) } @@ -1109,7 +1117,7 @@ internal class NestedScrollHandlerImpl( internal const val OffsetVisibilityThreshold = 0.5f private object NoOpDragController : DragController { - override fun onDrag(delta: Float) {} + override fun onDrag(delta: Float) = 0f - override fun onStop(velocity: Float, canChangeScene: Boolean) {} + override fun onStop(velocity: Float, canChangeScene: Boolean) = 0f } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index 734241e2faf6..d3e2a1cfbb3f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -35,7 +35,7 @@ import kotlinx.coroutines.launch @Composable internal fun PredictiveBackHandler( - state: BaseSceneTransitionLayoutState, + state: MutableSceneTransitionLayoutStateImpl, coroutineScope: CoroutineScope, targetSceneForBack: SceneKey? = null, ) { @@ -65,7 +65,7 @@ internal fun PredictiveBackHandler( } private class PredictiveBackTransition( - val state: BaseSceneTransitionLayoutState, + val state: MutableSceneTransitionLayoutStateImpl, val coroutineScope: CoroutineScope, fromScene: SceneKey, toScene: SceneKey, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 82275a9ac0a6..2fc4526b31f2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -514,7 +514,7 @@ internal fun SceneTransitionLayoutForTesting( val coroutineScope = rememberCoroutineScope() val layoutImpl = remember { SceneTransitionLayoutImpl( - state = state as BaseSceneTransitionLayoutState, + state = state as MutableSceneTransitionLayoutStateImpl, density = density, layoutDirection = layoutDirection, swipeSourceDetector = swipeSourceDetector, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 3e48c429ba7d..32db0b7cd9fe 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -44,7 +44,7 @@ internal typealias MovableElementContent = @Composable (@Composable () -> Unit) @Stable internal class SceneTransitionLayoutImpl( - internal val state: BaseSceneTransitionLayoutState, + internal val state: MutableSceneTransitionLayoutStateImpl, internal var density: Density, internal var layoutDirection: LayoutDirection, internal var swipeSourceDetector: SwipeSourceDetector, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 56c8752eb53c..48bc251f1f87 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -200,7 +200,7 @@ sealed interface TransitionState { * transition. * * Important: These will be set exactly once, when this transition is - * [started][BaseSceneTransitionLayoutState.startTransition]. + * [started][MutableSceneTransitionLayoutStateImpl.startTransition]. */ internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty private var fromOverscrollSpec: OverscrollSpecImpl? = null @@ -332,13 +332,16 @@ sealed interface TransitionState { } } -internal abstract class BaseSceneTransitionLayoutState( +/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */ +internal class MutableSceneTransitionLayoutStateImpl( initialScene: SceneKey, - protected var stateLinks: List<StateLink>, + override var transitions: SceneTransitions = transitions {}, + internal val canChangeScene: (SceneKey) -> Boolean = { true }, + private val stateLinks: List<StateLink> = emptyList(), // TODO(b/290930950): Remove this flag. - internal var enableInterruptions: Boolean, -) : SceneTransitionLayoutState { + internal val enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, +) : MutableSceneTransitionLayoutState { private val creationThread: Thread = Thread.currentThread() /** @@ -374,17 +377,6 @@ internal abstract class BaseSceneTransitionLayoutState( @VisibleForTesting internal val finishedTransitions = mutableMapOf<TransitionState.Transition, SceneKey>() - /** Whether we can transition to the given [scene]. */ - internal abstract fun canChangeScene(scene: SceneKey): Boolean - - /** - * Called when the [current scene][TransitionState.currentScene] should be changed to [scene]. - * - * When this is called, the source of truth for the current scene should be changed so that - * [transitionState] will animate and settle to [scene]. - */ - internal abstract fun CoroutineScope.onChangeScene(scene: SceneKey) - internal fun checkThread() { val current = Thread.currentThread() if (current !== creationThread) { @@ -409,6 +401,20 @@ internal abstract class BaseSceneTransitionLayoutState( return transition.isTransitioningBetween(scene, other) } + override fun setTargetScene( + targetScene: SceneKey, + coroutineScope: CoroutineScope, + transitionKey: TransitionKey?, + ): TransitionState.Transition? { + checkThread() + + return coroutineScope.animateToScene( + layoutState = this@MutableSceneTransitionLayoutStateImpl, + target = targetScene, + transitionKey = transitionKey, + ) + } + /** * Start a new [transition]. * @@ -600,7 +606,7 @@ internal abstract class BaseSceneTransitionLayoutState( } } - fun snapToScene(scene: SceneKey) { + override fun snapToScene(scene: SceneKey) { checkThread() // Force finish all transitions. @@ -674,37 +680,6 @@ internal abstract class BaseSceneTransitionLayoutState( } } -/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */ -internal class MutableSceneTransitionLayoutStateImpl( - initialScene: SceneKey, - override var transitions: SceneTransitions = transitions {}, - private val canChangeScene: (SceneKey) -> Boolean = { true }, - stateLinks: List<StateLink> = emptyList(), - enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, -) : - MutableSceneTransitionLayoutState, - BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) { - override fun setTargetScene( - targetScene: SceneKey, - coroutineScope: CoroutineScope, - transitionKey: TransitionKey?, - ): TransitionState.Transition? { - checkThread() - - return coroutineScope.animateToScene( - layoutState = this@MutableSceneTransitionLayoutStateImpl, - target = targetScene, - transitionKey = transitionKey, - ) - } - - override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene) - - override fun CoroutineScope.onChangeScene(scene: SceneKey) { - setTargetScene(scene, coroutineScope = this) - } -} - private const val TAG = "SceneTransitionLayoutState" /** Whether support for interruptions in enabled by default. */ diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt index 6c299463f978..2018d6e37b57 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt @@ -16,7 +16,7 @@ package com.android.compose.animation.scene.transition.link -import com.android.compose.animation.scene.BaseSceneTransitionLayoutState +import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutState import com.android.compose.animation.scene.TransitionKey @@ -25,7 +25,7 @@ import com.android.compose.animation.scene.TransitionState /** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */ class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) { - internal val target = target as BaseSceneTransitionLayoutState + internal val target = target as MutableSceneTransitionLayoutStateImpl /** * Links two transitions (source and target) together. diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 7a5a84e2c3f1..c8bbb149a042 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -212,7 +212,8 @@ class DraggableHandlerTest { draggableHandler: DraggableHandler, startedPosition: Offset = Offset.Zero, overSlop: Float = 0f, - pointersDown: Int = 1 + pointersDown: Int = 1, + expectedConsumed: Boolean = true, ): DragController { val dragController = draggableHandler.onDragStarted( @@ -222,17 +223,23 @@ class DraggableHandlerTest { ) // MultiPointerDraggable will always call onDelta with the initial overSlop right after - dragController.onDragDelta(pixels = overSlop) + dragController.onDragDelta(pixels = overSlop, expectedConsumed = expectedConsumed) return dragController } - fun DragController.onDragDelta(pixels: Float) { - onDrag(delta = pixels) + fun DragController.onDragDelta(pixels: Float, expectedConsumed: Boolean = true) { + val consumed = onDrag(delta = pixels) + assertThat(consumed).isEqualTo(if (expectedConsumed) pixels else 0f) } - fun DragController.onDragStopped(velocity: Float, canChangeScene: Boolean = true) { - onStop(velocity, canChangeScene) + fun DragController.onDragStopped( + velocity: Float, + canChangeScene: Boolean = true, + expectedConsumed: Boolean = true + ) { + val consumed = onStop(velocity, canChangeScene) + assertThat(consumed).isEqualTo(if (expectedConsumed) velocity else 0f) } fun NestedScrollConnection.scroll( @@ -360,10 +367,18 @@ class DraggableHandlerTest { @Test fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest { - onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f)) + onDragStarted( + horizontalDraggableHandler, + overSlop = up(fractionOfScreen = 0.3f), + expectedConsumed = false, + ) assertIdle(currentScene = SceneA) - onDragStarted(horizontalDraggableHandler, overSlop = down(fractionOfScreen = 0.3f)) + onDragStarted( + horizontalDraggableHandler, + overSlop = down(fractionOfScreen = 0.3f), + expectedConsumed = false, + ) assertIdle(currentScene = SceneA) } @@ -489,19 +504,19 @@ class DraggableHandlerTest { // start accelaratedScroll and scroll over to B -> null val dragController2 = onDragStartedImmediately() - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may // still be called. Make sure that they don't crash or change the scene - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) dragController2.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(SceneB) // These events can still come in after the animation has settled - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) dragController2.onDragStopped(velocity = 0f) assertIdle(SceneB) } @@ -845,7 +860,7 @@ class DraggableHandlerTest { assertThat(progress).isEqualTo(0.2f) // this should be ignored, we are scrolling now! - dragController.onDragStopped(-velocityThreshold) + dragController.onDragStopped(-velocityThreshold, expectedConsumed = false) assertTransition(currentScene = SceneA) nestedScroll.scroll(available = -offsetY10) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index ecafb170535d..b98400a70ea4 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -49,6 +49,21 @@ import org.junit.runner.RunWith class MultiPointerDraggableTest { @get:Rule val rule = createComposeRule() + private class SimpleDragController( + val onDrag: () -> Unit, + val onStop: () -> Unit, + ) : DragController { + override fun onDrag(delta: Float): Float { + onDrag() + return delta + } + + override fun onStop(velocity: Float, canChangeScene: Boolean): Float { + onStop() + return velocity + } + } + @Test fun cancellingPointerCallsOnDragStopped() { val size = 200f @@ -70,15 +85,10 @@ class MultiPointerDraggableTest { startDragImmediately = { false }, onDragStarted = { _, _, _ -> started = true - object : DragController { - override fun onDrag(delta: Float) { - dragged = true - } - - override fun onStop(velocity: Float, canChangeScene: Boolean) { - stopped = true - } - } + SimpleDragController( + onDrag = { dragged = true }, + onStop = { stopped = true }, + ) }, ) ) @@ -142,15 +152,10 @@ class MultiPointerDraggableTest { startDragImmediately = { true }, onDragStarted = { _, _, _ -> started = true - object : DragController { - override fun onDrag(delta: Float) { - dragged = true - } - - override fun onStop(velocity: Float, canChangeScene: Boolean) { - stopped = true - } - } + SimpleDragController( + onDrag = { dragged = true }, + onStop = { stopped = true }, + ) }, ) .pointerInput(Unit) { @@ -218,15 +223,10 @@ class MultiPointerDraggableTest { startDragImmediately = { false }, onDragStarted = { _, _, _ -> started = true - object : DragController { - override fun onDrag(delta: Float) { - dragged = true - } - - override fun onStop(velocity: Float, canChangeScene: Boolean) { - stopped = true - } - } + SimpleDragController( + onDrag = { dragged = true }, + onStop = { stopped = true }, + ) }, ) ) { @@ -341,15 +341,10 @@ class MultiPointerDraggableTest { startDragImmediately = { false }, onDragStarted = { _, _, _ -> started = true - object : DragController { - override fun onDrag(delta: Float) { - dragged = true - } - - override fun onStop(velocity: Float, canChangeScene: Boolean) { - stopped = true - } - } + SimpleDragController( + onDrag = { dragged = true }, + onStop = { stopped = true }, + ) }, ) ) { @@ -447,15 +442,10 @@ class MultiPointerDraggableTest { startDragImmediately = { false }, onDragStarted = { _, _, _ -> verticalStarted = true - object : DragController { - override fun onDrag(delta: Float) { - verticalDragged = true - } - - override fun onStop(velocity: Float, canChangeScene: Boolean) { - verticalStopped = true - } - } + SimpleDragController( + onDrag = { verticalDragged = true }, + onStop = { verticalStopped = true }, + ) }, ) .multiPointerDraggable( @@ -464,15 +454,10 @@ class MultiPointerDraggableTest { startDragImmediately = { false }, onDragStarted = { _, _, _ -> horizontalStarted = true - object : DragController { - override fun onDrag(delta: Float) { - horizontalDragged = true - } - - override fun onStop(velocity: Float, canChangeScene: Boolean) { - horizontalStopped = true - } - } + SimpleDragController( + onDrag = { horizontalDragged = true }, + onStop = { horizontalStopped = true }, + ) }, ) ) @@ -567,11 +552,10 @@ class MultiPointerDraggableTest { }, onDragStarted = { _, _, _ -> started = true - object : DragController { - override fun onDrag(delta: Float) {} - - override fun onStop(velocity: Float, canChangeScene: Boolean) {} - } + SimpleDragController( + onDrag = { /* do nothing */ }, + onStop = { /* do nothing */ }, + ) }, ) ) {} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 41bf630e9ae4..52cceecadbf9 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -153,7 +153,7 @@ class SceneTransitionLayoutStateTest { sourceTo: SceneKey? = SceneB, targetFrom: SceneKey? = SceneC, targetTo: SceneKey = SceneD - ): Pair<BaseSceneTransitionLayoutState, BaseSceneTransitionLayoutState> { + ): Pair<MutableSceneTransitionLayoutStateImpl, MutableSceneTransitionLayoutStateImpl> { val parentState = MutableSceneTransitionLayoutState(parentInitialScene) val link = listOf( @@ -164,8 +164,8 @@ class SceneTransitionLayoutStateTest { ) val childState = MutableSceneTransitionLayoutState(childInitialScene, stateLinks = link) return Pair( - parentState as BaseSceneTransitionLayoutState, - childState as BaseSceneTransitionLayoutState + parentState as MutableSceneTransitionLayoutStateImpl, + childState as MutableSceneTransitionLayoutStateImpl ) } @@ -187,7 +187,7 @@ class SceneTransitionLayoutStateTest { @Test fun linkedTransition_transitiveLink() { val parentParentState = - MutableSceneTransitionLayoutState(SceneB) as BaseSceneTransitionLayoutState + MutableSceneTransitionLayoutState(SceneB) as MutableSceneTransitionLayoutStateImpl val parentLink = listOf( StateLink( @@ -197,7 +197,7 @@ class SceneTransitionLayoutStateTest { ) val parentState = MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink) - as BaseSceneTransitionLayoutState + as MutableSceneTransitionLayoutStateImpl val link = listOf( StateLink( @@ -207,7 +207,7 @@ class SceneTransitionLayoutStateTest { ) val childState = MutableSceneTransitionLayoutState(SceneA, stateLinks = link) - as BaseSceneTransitionLayoutState + as MutableSceneTransitionLayoutStateImpl val childTransition = transition(SceneA, SceneB) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt index 5cf4f16aed62..7fd9ce20ab92 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt @@ -146,6 +146,14 @@ class ShadeTouchHandlerTest : SysuiTestCase() { verify(mShadeViewController, never()).handleExternalTouch(any()) } + @Test + fun testCancelMotionEvent_popsTouchSession() { + swipe(Direction.DOWN) + val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0) + mInputListenerCaptor.lastValue.onInputEvent(event) + verify(mTouchSession).pop() + } + /** * Simulates a swipe in the given direction and returns true if the touch was intercepted by the * touch handler's gesture listener. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index a8bdc7c632d2..1f5e30ca4a0d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -88,24 +88,6 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { } @Test - fun isDisclaimerDismissed_byDefault_isFalse() = - testScope.runTest { - val isDisclaimerDismissed by - collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER)) - assertThat(isDisclaimerDismissed).isFalse() - } - - @Test - fun isDisclaimerDismissed_onSet_isTrue() = - testScope.runTest { - val isDisclaimerDismissed by - collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER)) - - underTest.setDisclaimerDismissed(MAIN_USER) - assertThat(isDisclaimerDismissed).isTrue() - } - - @Test fun getSharedPreferences_whenFileRestored() = testScope.runTest { val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 5cdbe9ce5856..9539c0492056 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -84,6 +84,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -1059,6 +1060,25 @@ class CommunalInteractorTest : SysuiTestCase() { ) } + @Test + fun dismissDisclaimerSetsDismissedFlag() = + testScope.runTest { + val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) + assertThat(disclaimerDismissed).isFalse() + underTest.setDisclaimerDismissed() + assertThat(disclaimerDismissed).isTrue() + } + + @Test + fun dismissDisclaimerTimeoutResetsDismissedFlag() = + testScope.runTest { + val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) + underTest.setDisclaimerDismissed() + assertThat(disclaimerDismissed).isTrue() + advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + assertThat(disclaimerDismissed).isFalse() + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt index 7b79d2817478..9a92f76f90c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt @@ -74,40 +74,6 @@ class CommunalPrefsInteractorTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } - @Test - fun setDisclaimerDismissed_currentUser() = - testScope.runTest { - setSelectedUser(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isFalse() - underTest.setDisclaimerDismissed(MAIN_USER) - assertThat(isDisclaimerDismissed).isTrue() - } - - @Test - fun setDisclaimerDismissed_anotherUser() = - testScope.runTest { - setSelectedUser(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isFalse() - underTest.setDisclaimerDismissed(SECONDARY_USER) - assertThat(isDisclaimerDismissed).isFalse() - } - - @Test - fun isDisclaimerDismissed_userSwitch() = - testScope.runTest { - setSelectedUser(MAIN_USER) - underTest.setDisclaimerDismissed(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isTrue() - setSelectedUser(SECONDARY_USER) - assertThat(isDisclaimerDismissed).isFalse() - } - private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt new file mode 100644 index 000000000000..f7f70c154ce6 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt @@ -0,0 +1,685 @@ +/* + * 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.systemui.communal.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.ObservableTransitionState.Idle +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_COMMUNAL_HUB, FLAG_COMMUNAL_SCENE_KTF_REFACTOR) +@DisableSceneContainer +class CommunalSceneTransitionInteractorTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { keyguardTransitionRepository = realKeyguardTransitionRepository } + private val testScope = kosmos.testScope + + private val underTest by lazy { kosmos.communalSceneTransitionInteractor } + private val keyguardTransitionRepository by lazy { kosmos.realKeyguardTransitionRepository } + + private val ownerName = CommunalSceneTransitionInteractor::class.java.simpleName + private val progress = MutableSharedFlow<Float>() + + private val sceneTransitions = + MutableStateFlow<ObservableTransitionState>(Idle(CommunalScenes.Blank)) + + private val blankToHub = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Blank, + toScene = CommunalScenes.Communal, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + private val hubToBlank = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Communal), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + @Before + fun setup() { + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) + underTest.start() + kosmos.communalSceneRepository.setTransitionState(sceneTransitions) + testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) } + } + + /** Transition from blank to glanceable hub. This is the default case. */ + @Test + fun transition_from_blank_end_in_hub() = + testScope.runTest { + sceneTransitions.value = blankToHub + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + progress.emit(1f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 1f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = Idle(CommunalScenes.Communal) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } + + /** Transition from hub to lockscreen. */ + @Test + fun transition_from_hub_end_in_lockscreen() = + testScope.runTest { + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = Idle(CommunalScenes.Blank) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } + + /** Transition from hub to dream. */ + @Test + fun transition_from_hub_end_in_dream() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setDreaming(true) + runCurrent() + + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = DREAMING, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = DREAMING, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = Idle(CommunalScenes.Blank) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = DREAMING, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } + + /** Transition from blank to hub, then settle back in blank. */ + @Test + fun transition_from_blank_end_in_blank() = + testScope.runTest { + sceneTransitions.value = blankToHub + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + val allSteps by collectValues(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + val numToDrop = allSteps.size + // Settle back in blank + sceneTransitions.value = Idle(CommunalScenes.Blank) + + // Assert that KTF reversed transition back to lockscreen. + assertThat(allSteps.drop(numToDrop)) + .containsExactly( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = CANCELED, + value = 0.4f, + ownerName = ownerName, + ), + // Transition back to lockscreen + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0.6f, + ownerName = ownerName, + ), + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ), + ) + .inOrder() + } + + @Test + fun transition_to_occluded_with_changed_scene_respected_just_once() = + testScope.runTest { + underTest.onSceneAboutToChange(CommunalScenes.Blank, OCCLUDED) + runCurrent() + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = blankToHub + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = OCCLUDED, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = hubToBlank + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + } + + @Test + fun transition_from_blank_interrupted() = + testScope.runTest { + sceneTransitions.value = blankToHub + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + val allSteps by collectValues(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + val numToDrop = allSteps.size + // Transition back from hub to blank, interrupting + // the current transition. + sceneTransitions.value = hubToBlank + + assertThat(allSteps.drop(numToDrop)) + .containsExactly( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + value = 1f, + transitionState = FINISHED, + ownerName = ownerName, + ), + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + value = 0f, + transitionState = STARTED, + ownerName = ownerName, + ), + ) + .inOrder() + + progress.emit(0.1f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.1f, + ownerName = ownerName, + ) + ) + } + + /** + * Blank -> Hub transition interrupted by a new Blank -> Hub transition. KTF state should not be + * updated in this case. + */ + @Test + fun transition_to_hub_duplicate_does_not_change_ktf() = + testScope.runTest { + sceneTransitions.value = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Blank, + toScene = CommunalScenes.Communal, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + val allSteps by collectValues(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + val sizeBefore = allSteps.size + val newProgress = MutableSharedFlow<Float>() + sceneTransitions.value = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Blank, + toScene = CommunalScenes.Communal, + currentScene = flowOf(CommunalScenes.Blank), + progress = newProgress, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + + // No new KTF steps emitted as a result of the new transition. + assertThat(allSteps).hasSize(sizeBefore) + + // Progress is now tracked by the new flow. + newProgress.emit(0.1f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.1f, + ownerName = ownerName, + ) + ) + } + + /** + * STL: Hub -> Blank, then interrupt in KTF LS -> OCCLUDED, then STL still finishes in Blank. + * After a KTF transition is started (GLANCEABLE_HUB -> LOCKSCREEN) KTF immediately considers + * the active scene to be LOCKSCREEN. This means that all listeners for LOCKSCREEN are active + * and may start a new transition LOCKSCREEN -> *. Here we test LOCKSCREEN -> OCCLUDED. + * + * KTF is allowed to already start and play the other transition, while the STL transition may + * finish later (gesture completes much later). When we eventually settle the STL transition in + * Blank we do not want to force KTF back to its original destination (LOCKSCREEN). Instead, for + * this scenario the settle can be ignored. + */ + @Test + fun transition_to_blank_interrupted_by_ktf_transition_then_finish_in_blank() = + testScope.runTest { + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + // Start another transition externally while our scene + // transition is happening. + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = "external", + from = LOCKSCREEN, + to = OCCLUDED, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // Scene progress should not affect KTF transition anymore + progress.emit(0.7f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // Scene transition still finishes but should not impact KTF transition + sceneTransitions.value = Idle(CommunalScenes.Blank) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + } + + /** + * STL: Hub -> Blank, then interrupt in KTF LS -> OCCLUDED, then STL finishes back in Hub. + * + * This is similar to the previous scenario but the gesture may have been interrupted by any + * other transition. KTF needs to immediately finish in GLANCEABLE_HUB (there is a jump cut). + */ + @Test + fun transition_to_blank_interrupted_by_ktf_transition_then_finish_in_hub() = + testScope.runTest { + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + // Start another transition externally while our scene + // transition is happening. + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = "external", + from = LOCKSCREEN, + to = OCCLUDED, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // Scene progress should not affect KTF transition anymore + progress.emit(0.7f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // We land back in communal. + sceneTransitions.value = Idle(CommunalScenes.Communal) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = OCCLUDED, + to = GLANCEABLE_HUB, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index b138fb3b779a..f8906adc33d4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -65,6 +65,7 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -352,6 +353,21 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test + fun showDisclaimer_trueWhenTimeout() = + testScope.runTest { + underTest.setEditModeState(EditModeState.SHOWING) + kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + + val showDisclaimer by collectLastValue(underTest.showDisclaimer) + + assertThat(showDisclaimer).isTrue() + underTest.onDisclaimerDismissed() + assertThat(showDisclaimer).isFalse() + advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + assertThat(showDisclaimer).isTrue() + } + + @Test fun scrollPosition_persistedOnEditCleanup() { val index = 2 val offset = 30 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt index ac50db483301..e36fd75445e2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt @@ -14,17 +14,20 @@ * limitations under the License. */ +package com.android.systemui.communal.widgets + import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -34,6 +37,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.verify +@ExperimentalCoroutinesApi @SmallTest @RunWith(AndroidJUnit4::class) class CommunalTransitionAnimatorControllerTest : SysuiTestCase() { @@ -66,7 +70,7 @@ class CommunalTransitionAnimatorControllerTest : SysuiTestCase() { } @Test - fun animationCancelled_launchingWidgetStateIsClearedAndSceneIsNotChanged() { + fun animationCancelled_launchingWidgetStateIsCleared() { with(kosmos) { testScope.runTest { val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget) @@ -81,9 +85,12 @@ class CommunalTransitionAnimatorControllerTest : SysuiTestCase() { assertTrue(launching!!) verify(controller).onIntentStarted(willAnimate = true) + underTest.onTransitionAnimationStart(isExpandingFullyAbove = true) + assertTrue(launching!!) + verify(controller).onTransitionAnimationStart(isExpandingFullyAbove = true) + underTest.onTransitionAnimationCancelled(newKeyguardOccludedState = true) assertFalse(launching!!) - Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal) verify(controller).onTransitionAnimationCancelled(newKeyguardOccludedState = true) } } @@ -105,6 +112,12 @@ class CommunalTransitionAnimatorControllerTest : SysuiTestCase() { assertTrue(launching!!) verify(controller).onIntentStarted(willAnimate = true) + underTest.onTransitionAnimationStart(isExpandingFullyAbove = true) + assertTrue(launching!!) + verify(controller).onTransitionAnimationStart(isExpandingFullyAbove = true) + + testScope.advanceTimeBy(ActivityTransitionAnimator.TIMINGS.totalDuration) + underTest.onTransitionAnimationEnd(isExpandingFullyAbove = true) assertFalse(launching!!) Truth.assertThat(scene).isEqualTo(CommunalScenes.Blank) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 73ef77540398..88ba0411b414 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -84,7 +84,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { verify(mockAnimator, atLeastOnce()).addListener(captor.capture()) captor.allValues.forEach { it.onAnimationEnd(mockAnimator) } - verify(stateController).setExitAnimationsRunning(false) + verify(stateController, times(2)).setExitAnimationsRunning(false) } @Test @@ -154,4 +154,10 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { } ) } + + @Test + fun testCancelAnimations_clearsExitAnimationsRunning() { + controller.cancelAnimations() + verify(stateController).setExitAnimationsRunning(false) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt new file mode 100644 index 000000000000..ee51e37faba7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.education.data.repository.contextualEducationRepository +import com.android.systemui.education.data.repository.fakeEduClock +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.education.GestureType.BACK_GESTURE +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.keyboardTouchpadEduStatsInteractor + + @Test + fun dataUpdatedOnIncrementSignalCount() = + testScope.runTest { + val model by + collectLastValue( + kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE) + ) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK_GESTURE) + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataAddedOnUpdateShortcutTriggerTime() = + testScope.runTest { + val model by + collectLastValue( + kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE) + ) + assertThat(model?.lastShortcutTriggeredTime).isNull() + underTest.updateShortcutTriggerTime(BACK_GESTURE) + assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index 693fcdabea58..18839e6acb96 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -22,6 +22,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.testScope @@ -52,6 +53,7 @@ class QSLongPressEffectTest : SysuiTestCase() { private val vibratorHelper = kosmos.vibratorHelper private val qsTile = kosmos.qsTileFactory.createTile("Test Tile") @Mock private lateinit var callback: QSLongPressEffect.Callback + @Mock private lateinit var controller: ActivityTransitionAnimator.Controller private val effectDuration = 400 private val lowTickDuration = 12 @@ -218,8 +220,9 @@ class QSLongPressEffectTest : SysuiTestCase() { // GIVEN that the animation completes longPressEffect.handleAnimationComplete() - // THEN the effect ends in the idle state. + // THEN the effect ends in the idle state and the reversed callback is used. assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + verify(callback, times(1)).onEffectFinishedReversing() } @Test @@ -348,6 +351,23 @@ class QSLongPressEffectTest : SysuiTestCase() { assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE) } + @Test + fun onLongClickTransitionCancelled_whileInLongClickState_reversesEffect() = + testWhileInState(QSLongPressEffect.State.LONG_CLICKED) { + // GIVEN a transition controller delegate + val delegate = longPressEffect.createTransitionControllerDelegate(controller) + + // WHEN the activity launch animation is cancelled + val newOccludedState = false + delegate.onTransitionAnimationCancelled(newOccludedState) + + // THEN the effect reverses and ends in RUNNING_BACKWARDS_FROM_CANCEL + assertThat(longPressEffect.state) + .isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) + verify(callback, times(1)).onReverseAnimator(false) + verify(controller).onTransitionAnimationCancelled(newOccludedState) + } + private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt index 96b4b4325408..2b2c121fb79a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt @@ -171,14 +171,15 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun longPressed_openWppDirectlyEnabled_doesNotShowMenu_opensSettings() = + fun longPressed_isA11yAction_doesNotShowMenu_opensSettings() = testScope.runTest { - createUnderTest(isOpenWppDirectlyEnabled = true) + createUnderTest() val isMenuVisible by collectLastValue(underTest.isMenuVisible) val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings) + val isA11yAction = true runCurrent() - underTest.onLongPress() + underTest.onLongPress(isA11yAction) assertThat(isMenuVisible).isFalse() assertThat(shouldOpenSettings).isTrue() @@ -284,7 +285,6 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { private suspend fun createUnderTest( isLongPressFeatureEnabled: Boolean = true, isRevampedWppFeatureEnabled: Boolean = true, - isOpenWppDirectlyEnabled: Boolean = false, ) { // This needs to be re-created for each test outside of kosmos since the flag values are // read during initialization to set up flows. Maybe there is a better way to handle that. @@ -298,7 +298,6 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { featureFlags = kosmos.fakeFeatureFlagsClassic.apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled) - set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled) }, broadcastDispatcher = fakeBroadcastDispatcher, accessibilityManager = kosmos.accessibilityManagerWrapper, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index fc3b35d637f7..42cd5ec69466 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.app.StatusBarManager +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest @@ -25,9 +26,13 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.BrokenWithSceneContainer @@ -122,15 +127,22 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest private val fromGlanceableHubTransitionInteractor by lazy { kosmos.fromGlanceableHubTransitionInteractor } + private val communalSceneTransitionInteractor by lazy { + kosmos.communalSceneTransitionInteractor + } private val powerInteractor by lazy { kosmos.powerInteractor } private val communalInteractor by lazy { kosmos.communalInteractor } + private val communalSceneInteractor by lazy { kosmos.communalSceneInteractor } companion object { @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() + return FlagsParameterization.allCombinationsOf( + FLAG_COMMUNAL_SCENE_KTF_REFACTOR, + ) + .andSceneContainer() } } @@ -163,6 +175,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest fromOccludedTransitionInteractor.start() fromAlternateBouncerTransitionInteractor.start() fromGlanceableHubTransitionInteractor.start() + communalSceneTransitionInteractor.start() } @Test @@ -636,6 +649,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun dozingToGlanceableHub() = testScope.runTest { // GIVEN a prior transition has run to DOZING @@ -770,6 +784,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @BrokenWithSceneContainer(339465026) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun goneToGlanceableHub() = testScope.runTest { // GIVEN a prior transition has run to GONE @@ -799,6 +814,29 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } @Test + @BrokenWithSceneContainer(339465026) + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun goneToGlanceableHub_communalKtfRefactor() = + testScope.runTest { + // GIVEN a prior transition has run to GONE + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + + // WHEN the glanceable hub is shown + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + to = KeyguardState.GLANCEABLE_HUB, + from = KeyguardState.GONE, + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + animatorAssertion = { it.isNull() } + ) + + coroutineContext.cancelChildren() + } + + @Test @DisableSceneContainer fun alternateBouncerToPrimaryBouncer() = testScope.runTest { @@ -941,6 +979,11 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test fun alternateBouncerToGlanceableHub() = testScope.runTest { + // GIVEN the device is idle on the glanceable hub + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + clearInvocations(transitionRepository) + // GIVEN a prior transition has run to ALTERNATE_BOUNCER bouncerRepository.setAlternateVisible(true) runTransitionAndSetWakefulness( @@ -951,19 +994,11 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest // GIVEN the primary bouncer isn't showing and device not sleeping bouncerRepository.setPrimaryShow(false) - // GIVEN the device is idle on the glanceable hub - val idleTransitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(CommunalScenes.Communal) - ) - communalInteractor.setTransitionState(idleTransitionState) - runCurrent() - // WHEN the alternateBouncer stops showing bouncerRepository.setAlternateVisible(false) advanceTimeBy(200L) - // THEN a transition to LOCKSCREEN should occur + // THEN a transition to GLANCEABLE_HUB should occur assertThat(transitionRepository) .startedTransition( ownerName = FromAlternateBouncerTransitionInteractor::class.simpleName, @@ -1063,18 +1098,17 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @DisableSceneContainer fun primaryBouncerToGlanceableHub() = testScope.runTest { - // GIVEN a prior transition has run to PRIMARY_BOUNCER - bouncerRepository.setPrimaryShow(true) - runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER) - // GIVEN the device is idle on the glanceable hub - val idleTransitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(CommunalScenes.Communal) - ) - communalInteractor.setTransitionState(idleTransitionState) + communalSceneInteractor.changeScene(CommunalScenes.Communal) runCurrent() + // GIVEN a prior transition has run to PRIMARY_BOUNCER + bouncerRepository.setPrimaryShow(true) + runTransitionAndSetWakefulness( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.PRIMARY_BOUNCER + ) + // WHEN the primaryBouncer stops showing bouncerRepository.setPrimaryShow(false) runCurrent() @@ -1095,27 +1129,26 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @DisableSceneContainer fun primaryBouncerToGlanceableHubWhileDreaming() = testScope.runTest { + // GIVEN the device is idle on the glanceable hub + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + // GIVEN a prior transition has run to PRIMARY_BOUNCER bouncerRepository.setPrimaryShow(true) - runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER) + runTransitionAndSetWakefulness( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.PRIMARY_BOUNCER + ) // GIVEN that we are dreaming and occluded keyguardRepository.setDreaming(true) keyguardRepository.setKeyguardOccluded(true) - // GIVEN the device is idle on the glanceable hub - val idleTransitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(CommunalScenes.Communal) - ) - communalInteractor.setTransitionState(idleTransitionState) - runCurrent() - // WHEN the primaryBouncer stops showing bouncerRepository.setPrimaryShow(false) runCurrent() - // THEN a transition to LOCKSCREEN should occur + // THEN a transition to GLANCEABLE_HUB should occur assertThat(transitionRepository) .startedTransition( ownerName = FromPrimaryBouncerTransitionInteractor::class.simpleName, @@ -1219,6 +1252,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @BrokenWithSceneContainer(339465026) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun occludedToGlanceableHub() = testScope.runTest { // GIVEN a device on lockscreen @@ -1256,6 +1290,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @BrokenWithSceneContainer(339465026) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun occludedToGlanceableHubWhenInitiallyOnHub() = testScope.runTest { // GIVEN a device on lockscreen and communal is available @@ -1293,6 +1328,37 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } @Test + @BrokenWithSceneContainer(339465026) + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun occludedToGlanceableHub_communalKtfRefactor() = + testScope.runTest { + // GIVEN a device on lockscreen and communal is available + keyguardRepository.setKeyguardShowing(true) + kosmos.setCommunalAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB + runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED) + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + + // WHEN occlusion ends + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + // THEN a transition to GLANCEABLE_HUB should occur + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.OCCLUDED, + to = KeyguardState.GLANCEABLE_HUB, + animatorAssertion = { it.isNull() }, + ) + + coroutineContext.cancelChildren() + } + + @Test fun occludedToAlternateBouncer() = testScope.runTest { // GIVEN a prior transition has run to OCCLUDED @@ -1511,6 +1577,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun dreamingToGlanceableHub() = testScope.runTest { // GIVEN a prior transition has run to DREAMING @@ -1550,24 +1617,41 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } @Test - @BrokenWithSceneContainer(339465026) - fun lockscreenToOccluded() = + @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun dreamingToGlanceableHub_communalKtfRefactor() = testScope.runTest { - // GIVEN a prior transition has run to LOCKSCREEN - runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.LOCKSCREEN) + // GIVEN a prior transition has run to DREAMING + keyguardRepository.setDreaming(true) + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING) runCurrent() - // WHEN the keyguard is occluded - keyguardRepository.setKeyguardOccluded(true) + // WHEN a transition to the glanceable hub starts + val currentScene = CommunalScenes.Blank + val targetScene = CommunalScenes.Communal + + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = currentScene, + toScene = targetScene, + currentScene = flowOf(targetScene), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .1f runCurrent() - // THEN a transition to OCCLUDED should occur assertThat(transitionRepository) .startedTransition( - ownerName = "FromLockscreenTransitionInteractor", - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, - animatorAssertion = { it.isNotNull() }, + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.DREAMING, + to = KeyguardState.GLANCEABLE_HUB, + animatorAssertion = { it.isNull() }, // transition should be manually animated ) coroutineContext.cancelChildren() @@ -1575,10 +1659,10 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @BrokenWithSceneContainer(339465026) - fun aodToOccluded() = + fun lockscreenToOccluded() = testScope.runTest { - // GIVEN a prior transition has run to AOD - runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD) + // GIVEN a prior transition has run to LOCKSCREEN + runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.LOCKSCREEN) runCurrent() // WHEN the keyguard is occluded @@ -1588,8 +1672,8 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest // THEN a transition to OCCLUDED should occur assertThat(transitionRepository) .startedTransition( - ownerName = "FromAodTransitionInteractor(isOccluded = true)", - from = KeyguardState.AOD, + ownerName = "FromLockscreenTransitionInteractor", + from = KeyguardState.LOCKSCREEN, to = KeyguardState.OCCLUDED, animatorAssertion = { it.isNotNull() }, ) @@ -1703,6 +1787,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun lockscreenToGlanceableHub() = testScope.runTest { // GIVEN a prior transition has run to LOCKSCREEN @@ -1761,6 +1846,48 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun lockscreenToGlanceableHub_communalKtfRefactor() = + testScope.runTest { + // GIVEN a prior transition has run to LOCKSCREEN + runTransitionAndSetWakefulness(KeyguardState.AOD, KeyguardState.LOCKSCREEN) + runCurrent() + + // WHEN a glanceable hub transition starts + val currentScene = CommunalScenes.Blank + val targetScene = CommunalScenes.Communal + + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = currentScene, + toScene = targetScene, + currentScene = flowOf(targetScene), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .1f + runCurrent() + + // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + animatorAssertion = { it.isNull() }, // transition should be manually animated + ) + + coroutineContext.cancelChildren() + } + + @Test + @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToLockscreen() = testScope.runTest { // GIVEN a prior transition has run to GLANCEABLE_HUB @@ -1816,6 +1943,48 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun glanceableHubToLockscreen_communalKtfRefactor() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + clearInvocations(transitionRepository) + + // WHEN a transition away from glanceable hub starts + val currentScene = CommunalScenes.Communal + val targetScene = CommunalScenes.Blank + + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = currentScene, + toScene = targetScene, + currentScene = flowOf(targetScene), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .1f + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + animatorAssertion = { it.isNull() }, // transition should be manually animated + ) + + coroutineContext.cancelChildren() + } + + @Test + @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToDozing() = testScope.runTest { // GIVEN a prior transition has run to GLANCEABLE_HUB @@ -1838,6 +2007,31 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun glanceableHubToDozing_communalKtfRefactor() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + clearInvocations(transitionRepository) + + // WHEN the device begins to sleep + powerInteractor.setAsleepForTest() + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.DOZING, + animatorAssertion = { it.isNull() }, + ) + + coroutineContext.cancelChildren() + } + + @Test + @DisableSceneContainer fun glanceableHubToPrimaryBouncer() = testScope.runTest { // GIVEN a prior transition has run to ALTERNATE_BOUNCER @@ -1882,6 +2076,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @BrokenWithSceneContainer(339465026) + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToOccluded() = testScope.runTest { // GIVEN a prior transition has run to GLANCEABLE_HUB @@ -1912,7 +2107,33 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } @Test + @BrokenWithSceneContainer(339465026) + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun glanceableHubToOccluded_communalKtfRefactor() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + clearInvocations(transitionRepository) + + // WHEN the keyguard is occluded + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.OCCLUDED, + animatorAssertion = { it.isNull() }, + ) + + coroutineContext.cancelChildren() + } + + @Test @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToGone() = testScope.runTest { // GIVEN a prior transition has run to GLANCEABLE_HUB @@ -1935,6 +2156,32 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun glanceableHubToGone_communalKtfRefactor() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + clearInvocations(transitionRepository) + + // WHEN keyguard goes away + keyguardRepository.setKeyguardGoingAway(true) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.GONE, + animatorAssertion = { it.isNull() }, + ) + + coroutineContext.cancelChildren() + } + + @Test + @DisableSceneContainer + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToDreaming() = testScope.runTest { // GIVEN that we are dreaming and not dozing @@ -1963,7 +2210,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest isUserInputOngoing = flowOf(false), ) ) - communalInteractor.setTransitionState(transitionState) + communalSceneInteractor.setTransitionState(transitionState) runCurrent() assertThat(transitionRepository) @@ -1977,6 +2224,52 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest coroutineContext.cancelChildren() } + @Test + @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) + fun glanceableHubToDreaming_communalKtfRefactor() = + testScope.runTest { + // GIVEN that we are dreaming and not dozing + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + runCurrent() + + // GIVEN a prior transition has run to GLANCEABLE_HUB + communalSceneInteractor.changeScene(CommunalScenes.Communal) + runCurrent() + clearInvocations(transitionRepository) + + // WHEN a transition away from glanceable hub starts + val currentScene = CommunalScenes.Communal + val targetScene = CommunalScenes.Blank + + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = currentScene, + toScene = targetScene, + currentScene = flowOf(targetScene), + progress = flowOf(0f, 0.1f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.DREAMING, + animatorAssertion = { it.isNull() }, // transition should be manually animated + ) + + coroutineContext.cancelChildren() + } + private suspend fun TestScope.runTransitionAndSetWakefulness( from: KeyguardState, to: KeyguardState diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 278c90a5f068..fd2e33504ec9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -159,8 +159,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { ) assertThat(values[0]).isEqualTo(1f) - // Should fade to zero between here assertThat(values[1]).isEqualTo(0f) + // Should always finish with 1f to show HUNs + assertThat(values[2]).isEqualTo(1f) } @Test @@ -177,7 +178,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(3) // Shade stays open, and alpha should remain visible values.forEach { assertThat(it).isEqualTo(1f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index a120bdc0b743..540a85aeb695 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -175,12 +175,14 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) assertThat(isVisible).isFalse() - kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = + kosmos.headsUpNotificationRepository.setNotifications( buildNotificationRows(isPinned = true) + ) assertThat(isVisible).isTrue() - kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = + kosmos.headsUpNotificationRepository.setNotifications( buildNotificationRows(isPinned = false) + ) assertThat(isVisible).isFalse() } @@ -1699,8 +1701,8 @@ class SceneContainerStartableTest : SysuiTestCase() { return transitionStateFlow } - private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> = - setOf( + private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> = + listOf( fakeHeadsUpRowRepository(key = "0", isPinned = isPinned), fakeHeadsUpRowRepository(key = "1", isPinned = isPinned), fakeHeadsUpRowRepository(key = "2", isPinned = isPinned), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt new file mode 100644 index 000000000000..8810ade1d851 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt @@ -0,0 +1,510 @@ +/* + * 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. + */ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.app.Notification +import android.app.NotificationManager.IMPORTANCE_DEFAULT +import android.app.NotificationManager.IMPORTANCE_LOW +import android.os.UserHandle +import android.platform.test.annotations.EnableFlags +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.modifyEntry +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +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.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME) +class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { + testDispatcher = UnconfinedTestDispatcher() + statusBarStateController = + mock<SysuiStatusBarStateController>().also { mock -> + doAnswer { statusBarState.ordinal }.whenever(mock).state + } + fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + } + private val notifPipeline: NotifPipeline = mock() + private var statusBarState: StatusBarState = StatusBarState.KEYGUARD + + @Test + fun topUnseenSectioner() { + val solo = NotificationEntryBuilder().setTag("solo").build() + val child1 = NotificationEntryBuilder().setTag("child1").build() + val child2 = NotificationEntryBuilder().setTag("child2").build() + val parent = NotificationEntryBuilder().setTag("parent").build() + val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build() + + runCoordinatorTest { + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key + assertThat(topUnseenSectioner.isInSection(solo)).isTrue() + assertThat(topUnseenSectioner.isInSection(child1)).isFalse() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isFalse() + assertThat(topUnseenSectioner.isInSection(group)).isFalse() + + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key + assertThat(topUnseenSectioner.isInSection(solo)).isFalse() + assertThat(topUnseenSectioner.isInSection(child1)).isTrue() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isFalse() + assertThat(topUnseenSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key + assertThat(topUnseenSectioner.isInSection(solo)).isFalse() + assertThat(topUnseenSectioner.isInSection(child1)).isFalse() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isTrue() + assertThat(topUnseenSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null + assertThat(topUnseenSectioner.isInSection(solo)).isFalse() + assertThat(topUnseenSectioner.isInSection(child1)).isFalse() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isFalse() + assertThat(topUnseenSectioner.isInSection(group)).isFalse() + } + } + + @Test + fun topOngoingSectioner() { + val solo = NotificationEntryBuilder().setTag("solo").build() + val child1 = NotificationEntryBuilder().setTag("child1").build() + val child2 = NotificationEntryBuilder().setTag("child2").build() + val parent = NotificationEntryBuilder().setTag("parent").build() + val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build() + + runCoordinatorTest { + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key + assertThat(topOngoingSectioner.isInSection(solo)).isTrue() + assertThat(topOngoingSectioner.isInSection(child1)).isFalse() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isFalse() + assertThat(topOngoingSectioner.isInSection(group)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key + assertThat(topOngoingSectioner.isInSection(solo)).isFalse() + assertThat(topOngoingSectioner.isInSection(child1)).isTrue() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isFalse() + assertThat(topOngoingSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key + assertThat(topOngoingSectioner.isInSection(solo)).isFalse() + assertThat(topOngoingSectioner.isInSection(child1)).isFalse() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isTrue() + assertThat(topOngoingSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key + assertThat(topOngoingSectioner.isInSection(solo)).isFalse() + assertThat(topOngoingSectioner.isInSection(child1)).isFalse() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isFalse() + assertThat(topOngoingSectioner.isInSection(group)).isFalse() + } + } + + @Test + fun testPromoter() { + val child1 = NotificationEntryBuilder().setTag("child1").build() + val child2 = NotificationEntryBuilder().setTag("child2").build() + val child3 = NotificationEntryBuilder().setTag("child3").build() + val parent = NotificationEntryBuilder().setTag("parent").build() + GroupEntryBuilder() + .addChild(child1) + .addChild(child2) + .addChild(child3) + .setSummary(parent) + .build() + + runCoordinatorTest { + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null + assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null + assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue() + assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key + assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child2)) + .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key + assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue() + assertThat(promoter.shouldPromoteToTopLevel(child2)) + .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + } + } + + @Test + fun topOngoingIdentifier() { + val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build() + val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build() + val parent = defaultEntryBuilder().setTag("parent").setRank(3).build() + val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build() + val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build() + val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build() + val listEntryList = listOf(group, solo1, solo2) + + runCoordinatorTest { + // TEST: base case - no entries in the list + onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList()) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: none of these are unseen or ongoing yet, so don't pick them + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: when solo2 is the only one colorized, it gets picked up + solo2.setColorizedFgs(true) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo2.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: once solo1 is colorized, it takes priority for being ranked higher + solo1.setColorizedFgs(true) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo1.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: changing just the rank of solo1 causes it to pick up solo2 instead + solo1.modifyEntry { setRank(20) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo2.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: switching to SHADE disables the whole thing + statusBarState = StatusBarState.SHADE + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: switching back to KEYGUARD picks up the same entry again + statusBarState = StatusBarState.KEYGUARD + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo2.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: updating to not colorized revokes the top-ongoing status + solo2.setColorizedFgs(false) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo1.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: updating the importance to LOW revokes top-ongoing status + solo1.modifyEntry { setImportance(IMPORTANCE_LOW) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + } + } + + @Test + fun topUnseenIdentifier() { + val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build() + val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build() + val parent = defaultEntryBuilder().setTag("parent").setRank(4).build() + val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build() + val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build() + val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build() + val listEntryList = listOf(group, solo1, solo2) + val notificationEntryList = listOf(solo1, solo2, parent, child1, child2) + + runCoordinatorTest { + // All entries are added (and now unseen) + notificationEntryList.forEach { collectionListener.onEntryAdded(it) } + + // TEST: Filtered out entries are ignored + onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList()) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: top-ranked unseen child is selected (not the summary) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group)) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(child1.key) + + // TEST: top-ranked entry is picked + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen + solo1.setColorizedFgs(true) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo1.key) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: no more colorized entries + solo1.setColorizedFgs(false) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: if the rank of solo1 is reduced, solo2 will be preferred + solo1.modifyEntry { setRank(3) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: switching to SHADE state will disable the entire selector + statusBarState = StatusBarState.SHADE + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: switching back to KEYGUARD re-enables the selector + statusBarState = StatusBarState.KEYGUARD + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: QS Expansion does not mark entries as seen + setShadeAndQsExpansionThenWait(0f, 1f) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: Shade expansion does mark entries as seen + setShadeAndQsExpansionThenWait(1f, 0f) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: Entries updated while shade is expanded are NOT marked unseen + collectionListener.onEntryUpdated(solo1) + collectionListener.onEntryUpdated(solo2) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: Entries updated after shade is collapsed ARE marked unseen + setShadeAndQsExpansionThenWait(0f, 0f) + collectionListener.onEntryUpdated(solo1) + collectionListener.onEntryUpdated(solo2) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: low importance disqualifies the entry for top unseen + solo2.modifyEntry { setImportance(IMPORTANCE_LOW) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo1.key) + } + } + + @Test + fun topUnseenIdentifier_headsUpMarksSeen() { + val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build() + val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build() + val listEntryList = listOf(solo1, solo2) + val notificationEntryList = listOf(solo1, solo2) + + val hunRepo1 = solo1.fakeHeadsUpRowRepository() + val hunRepo2 = solo2.fakeHeadsUpRowRepository() + + runCoordinatorTest { + // All entries are added (and now unseen) + notificationEntryList.forEach { collectionListener.onEntryAdded(it) } + + // TEST: top-ranked entry is picked + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: heads up state and waiting isn't enough to be seen + kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = + listOf(hunRepo1, hunRepo2) + testScheduler.advanceTimeBy(1.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: even being pinned doesn't take effect immediately + hunRepo1.isPinned.value = true + testScheduler.advanceTimeBy(0.5.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: after being pinned a full second, solo1 is seen + testScheduler.advanceTimeBy(0.5.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: repeat; being heads up and pinned for 1 second triggers seen + kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2) + hunRepo1.isPinned.value = false + hunRepo2.isPinned.value = true + testScheduler.advanceTimeBy(1.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(null) + } + } + + private fun NotificationEntry.fakeHeadsUpRowRepository() = + FakeHeadsUpRowRepository(key = key, elementKey = Any()) + + private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait( + shadeExpansion: Float, + qsExpansion: Float + ) { + kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion) + // The coordinator waits a fraction of a second for the shade expansion to stick. + testScheduler.advanceTimeBy(1.seconds) + } + + private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT) + + private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) { + kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline) + kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { + KeyguardCoordinatorTestScope( + kosmos.lockScreenMinimalismCoordinator, + kosmos.testScope, + kosmos.fakeSettings, + ) + .testBlock() + } + } + + private inner class KeyguardCoordinatorTestScope( + private val coordinator: LockScreenMinimalismCoordinator, + private val scope: TestScope, + private val fakeSettings: FakeSettings, + ) : CoroutineScope by scope { + fun assertThatTopOngoingKey(): StringSubject { + return assertThat( + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value + ) + } + + fun assertThatTopUnseenKey(): StringSubject { + return assertThat( + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value + ) + } + + val testScheduler: TestCoroutineScheduler + get() = scope.testScheduler + + val promoter: NotifPromoter + get() = coordinator.unseenNotifPromoter + + val topUnseenSectioner: NotifSectioner + get() = coordinator.topUnseenSectioner + + val topOngoingSectioner: NotifSectioner + get() = coordinator.topOngoingSectioner + + val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener = + argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) } + .lastValue + + val collectionListener: NotifCollectionListener = + argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue + + var showOnlyUnseenNotifsOnKeyguardSetting: Boolean + get() = + fakeSettings.getIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_CURRENT, + ) == 1 + set(value) { + fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + if (value) 1 else 2, + UserHandle.USER_CURRENT, + ) + } + } + + companion object { + + private fun NotificationEntry.setColorizedFgs(colorized: Boolean) { + sbn.notification.setColorizedFgs(colorized) + } + + private fun Notification.setColorizedFgs(colorized: Boolean) { + extras.putBoolean(Notification.EXTRA_COLORIZED, colorized) + flags = + if (colorized) { + flags or Notification.FLAG_FOREGROUND_SERVICE + } else { + flags and Notification.FLAG_FOREGROUND_SERVICE.inv() + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index ce134e64bf57..75ecb2c5307c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -41,8 +41,12 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.communal.shared.model.CommunalScenes; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeAnimationRepository; import com.android.systemui.shade.data.repository.ShadeRepository; @@ -91,6 +95,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Mock private HeadsUpManager mHeadsUpManager; @Mock private VisibilityLocationProvider mVisibilityLocationProvider; @Mock private VisualStabilityProvider mVisualStabilityProvider; + @Mock private VisualStabilityCoordinatorLogger mLogger; @Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mSBStateListenerCaptor; @@ -128,7 +133,9 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { mVisibilityLocationProvider, mVisualStabilityProvider, mWakefulnessLifecycle, - mKosmos.getCommunalInteractor()); + mKosmos.getCommunalInteractor(), + mKosmos.getKeyguardTransitionInteractor(), + mLogger); mCoordinator.attach(mNotifPipeline); mTestScope.getTestScheduler().runCurrent(); @@ -241,6 +248,38 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() { + // GIVEN the panel true expanded and device isn't pulsing + setFullyDozed(false); + setSleepy(false); + setLockscreenShowing(0.5f); + setPulsing(false); + + // THEN group changes are NOT allowed + assertFalse(mNotifStabilityManager.isGroupChangeAllowed(mEntry)); + assertFalse(mNotifStabilityManager.isGroupPruneAllowed(mGroupEntry)); + + // THEN section changes are NOT allowed + assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry)); + } + + @Test + public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() { + // GIVEN the panel true expanded and device isn't pulsing + setFullyDozed(false); + setSleepy(false); + setLockscreenShowing(1.0f); + setPulsing(false); + + // THEN group changes are NOT allowed + assertFalse(mNotifStabilityManager.isGroupChangeAllowed(mEntry)); + assertFalse(mNotifStabilityManager.isGroupPruneAllowed(mGroupEntry)); + + // THEN section changes are NOT allowed + assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry)); + } + + @Test public void testPulsing_screenOff_groupAndSectionChangesNotAllowed() { // GIVEN the device is pulsing and screen is off setFullyDozed(true); @@ -614,7 +653,37 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } private void setPanelExpanded(boolean expanded) { - mStatusBarStateListener.onExpandedChanged(expanded); + setPanelExpandedAndLockscreenShowing(expanded, /* lockscreenShowing = */ 0.0f); } + private void setLockscreenShowing(float lockscreenShowing) { + setPanelExpandedAndLockscreenShowing(/* panelExpanded = */ false, lockscreenShowing); + } + + private void setPanelExpandedAndLockscreenShowing(boolean panelExpanded, + float lockscreenShowing) { + if (SceneContainerFlag.isEnabled()) { + mStatusBarStateListener.onExpandedChanged(panelExpanded); + mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava( + mTestScope, + makeLockscreenTransitionStep(lockscreenShowing), + /* validateStep = */ false); + } else { + mStatusBarStateListener.onExpandedChanged(panelExpanded || lockscreenShowing > 0.0f); + } + } + + private TransitionStep makeLockscreenTransitionStep(float value) { + if (value <= 0.0f) { + return new TransitionStep(KeyguardState.GONE); + } else if (value >= 1.0f) { + return new TransitionStep(KeyguardState.LOCKSCREEN); + } else { + return new TransitionStep( + KeyguardState.GONE, + KeyguardState.LOCKSCREEN, + value, + TransitionState.RUNNING); + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 8b4265f552fe..14134ccc34d0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRo import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt index 5e87f4663d76..61873ad294e3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel +import android.app.Notification import android.app.PendingIntent import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -90,7 +91,8 @@ class TimerViewModelTest : SysuiTestCase() { name: String = "example", timeRemaining: Duration = Duration.ofMinutes(3), resumeIntent: PendingIntent? = null, - resetIntent: PendingIntent? = null + addMinuteAction: Notification.Action? = null, + resetAction: Notification.Action? = null ) = TimerContentModel( icon = icon, @@ -99,7 +101,8 @@ class TimerViewModelTest : SysuiTestCase() { Paused( timeRemaining = timeRemaining, resumeIntent = resumeIntent, - resetIntent = resetIntent, + addMinuteAction = addMinuteAction, + resetAction = resetAction, ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index f8e633731c42..f96cf1011fb8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -43,7 +43,6 @@ import com.android.systemui.statusbar.notification.data.repository.setActiveNoti import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.statusbar.policy.fakeConfigurationController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 71cd95f828ef..6f099310f9f7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -1096,7 +1096,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + // Resets to 1f after communal scene is hidden + assertThat(alpha).isEqualTo(1f) } @Test @@ -1151,7 +1152,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + assertThat(alpha).isEqualTo(1f) } @Test @@ -1208,7 +1209,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + // Resets to 1f after communal scene is hidden + assertThat(alpha).isEqualTo(1f) } @Test @@ -1263,7 +1265,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + assertThat(alpha).isEqualTo(1f) } private suspend fun TestScope.showLockscreen() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index b643968c6322..c3c5a48dc197 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -50,6 +50,7 @@ class ActivityStarterImplTest : SysuiTestCase() { statusBarStateController = statusBarStateController, mainExecutor = mainExecutor, legacyActivityStarter = { legacyActivityStarterInternal }, + activityStarterInternal = { activityStarterInternal }, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt index 10a2f6407849..179799503ac0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt @@ -147,6 +147,48 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { } @Test + fun startActivityDismissingKeyguard_dismissShadeWhenOccluded_runAfterKeyguardGone() { + val intent = mock(Intent::class.java) + `when`(keyguardStateController.isShowing).thenReturn(true) + `when`(keyguardStateController.isOccluded).thenReturn(true) + `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true)) + `when`(communalSettingsInteractor.isCommunalFlagEnabled()).thenReturn(false) + + underTest.startActivityDismissingKeyguard(intent, dismissShade = true) + mainExecutor.runAllReady() + + val actionCaptor = argumentCaptor<OnDismissAction>() + verify(statusBarKeyguardViewManager) + .dismissWithAction(actionCaptor.capture(), any(), anyBoolean(), eq(null)) + actionCaptor.firstValue.onDismiss() + mainExecutor.runAllReady() + + verify(statusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()) + } + + @Test + fun startActivityDismissingKeyguard_dismissShadeWhenOccluded_runImmediately() { + val intent = mock(Intent::class.java) + `when`(keyguardStateController.isShowing).thenReturn(true) + `when`(keyguardStateController.isOccluded).thenReturn(true) + `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true)) + `when`(communalSettingsInteractor.isCommunalFlagEnabled()).thenReturn(true) + + underTest.startActivityDismissingKeyguard(intent, dismissShade = true) + mainExecutor.runAllReady() + + val actionCaptor = argumentCaptor<OnDismissAction>() + verify(statusBarKeyguardViewManager) + .dismissWithAction(actionCaptor.capture(), any(), anyBoolean(), eq(null)) + actionCaptor.firstValue.onDismiss() + mainExecutor.runAllReady() + + verify(statusBarKeyguardViewManager, never()).addAfterKeyguardGoneRunnable(any()) + verify(activityTransitionAnimator) + .startIntentWithAnimation(eq(null), eq(false), eq(null), eq(false), any()) + } + + @Test fun startPendingIntentDismissingKeyguard_keyguardShowing_dismissWithAction() { val pendingIntent = mock(PendingIntent::class.java) `when`(pendingIntent.isActivity).thenReturn(true) @@ -231,7 +273,6 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { // extra activity options to set on pending intent val activityOptions = mock(ActivityOptions::class.java) activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR - activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false val bundleCaptor = argumentCaptor<Bundle>() startPendingIntentMaybeDismissingKeyguard( @@ -255,7 +296,8 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { bundleCaptor.capture() ) val options = ActivityOptions.fromBundle(bundleCaptor.firstValue) - assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse() + assertThat(options.getPendingIntentBackgroundActivityStartMode()) + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR) } @@ -342,7 +384,6 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) } - @EnableFlags(Flags.FLAG_COMMUNAL_HUB) @Test fun startPendingIntentDismissingKeyguard_transitionAnimator_animateCommunal() { val parent = FrameLayout(context) @@ -389,7 +430,6 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { ) } - @DisableFlags(Flags.FLAG_COMMUNAL_HUB) @Test fun startPendingIntentDismissingKeyguard_transitionAnimator_doNotAnimateCommunal() { val parent = FrameLayout(context) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index 495ab61600f9..8f9da3b2e1e3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -180,6 +180,23 @@ class AvalancheControllerTest : SysuiTestCase() { } @Test + fun testDelete_untracked_runnableRuns() { + val headsUpEntry = createHeadsUpEntry(id = 0) + + // None showing + mAvalancheController.headsUpEntryShowing = null + + // Nothing is next + mAvalancheController.clearNext() + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") + + // Runnable was run + Mockito.verify(runnableMock, Mockito.times(1)).run() + } + + @Test fun testDelete_isNext_removedFromNext_runnableNotRun() { // Entry is next val headsUpEntry = createHeadsUpEntry(id = 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt index d0ddbffecf9a..b91bde4fb417 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy import android.content.Context import android.os.Handler +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest @@ -33,6 +34,7 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.phone.ConfigurationControllerImpl @@ -42,6 +44,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.time.SystemClock import junit.framework.Assert @@ -237,6 +240,36 @@ class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : BaseHeadsUpManager } @Test + @EnableFlags(NotificationThrottleHun.FLAG_NAME) + fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() { + whenever(mVSProvider.isReorderingAllowed).thenReturn(false) + val hmp = createHeadsUpManagerPhone() + + val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val row = mock<ExpandableNotificationRow>() + whenever(row.showingPulsing()).thenReturn(false) + notifEntry.row = row + + hmp.showNotification(notifEntry) + Assert.assertTrue(notifEntry.isSeenInShade) + } + + @Test + @EnableFlags(NotificationThrottleHun.FLAG_NAME) + fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() { + whenever(mVSProvider.isReorderingAllowed).thenReturn(true) + val hmp = createHeadsUpManagerPhone() + + val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val row = mock<ExpandableNotificationRow>() + whenever(row.showingPulsing()).thenReturn(false) + notifEntry.row = row + + hmp.showNotification(notifEntry) + Assert.assertFalse(notifEntry.isSeenInShade) + } + + @Test fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() = testScope.runTest { // GIVEN diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml b/packages/SystemUI/res/drawable/checkbox_circle_shape.xml index 9128dca2080b..2b987e2d1fb1 100644 --- a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml +++ b/packages/SystemUI/res/drawable/checkbox_circle_shape.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 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. @@ -14,15 +14,19 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.frameworks.core.batteryusagestatsprototests"> - - <uses-permission android:name="android.permission.BATTERY_STATS"/> - - <instrumentation - android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.frameworks.core.batteryusagestatsprototests" - android:label="BatteryUsageStats Proto Tests" /> - -</manifest> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:bottom="12dp" + android:left="12dp" + android:right="12dp" + android:top="12dp"> + <selector> + <item + android:drawable="@drawable/ic_check_circle_filled_24dp" + android:state_checked="true" /> + <item + android:drawable="@drawable/ic_circle_outline_24dp" + android:state_checked="false" /> + </selector> + </item> +</layer-list> diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml new file mode 100644 index 000000000000..16e2a3db2e95 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?androidprv:attr/materialColorPrimary" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c5.52,0 10,-4.48 10,-10S17.52,2 12,2zM10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l5.66,-5.66l1.41,1.41L10.59,16.6z" /> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml b/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml new file mode 100644 index 000000000000..82fa4f08d0c8 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?androidprv:attr/materialColorPrimary" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" /> +</vector> diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index 5191895549b6..d7b94ec015ac 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -58,9 +58,10 @@ android:layout_width="wrap_content" android:layout_height="48dp" android:layout_marginStart="16dp" + android:button="@drawable/checkbox_circle_shape" android:checked="true" android:text="@string/backlinks_include_link" - android:textColor="?android:textColorSecondary" + android:textColor="?androidprv:attr/materialColorOnBackground" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/preview" app:layout_constraintStart_toEndOf="@id/cancel" diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml index f2bfbe5c960d..3a679e3c16cb 100644 --- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml +++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml @@ -33,7 +33,6 @@ android:id="@+id/icon" android:layout_width="24dp" android:layout_height="24dp" - android:src="@drawable/ic_close" app:tint="@android:color/white" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/label" @@ -88,11 +87,10 @@ /> <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" android:id="@+id/mainButton" android:layout_width="124dp" android:layout_height="wrap_content" - tools:text="Reset" - tools:drawableStart="@android:drawable/ic_menu_add" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/altButton" app:layout_constraintTop_toBottomOf="@id/bottomOfTop" @@ -101,15 +99,23 @@ /> <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" android:id="@+id/altButton" - tools:text="Reset" - tools:drawableStart="@android:drawable/ic_menu_add" - android:drawablePadding="2dp" - android:drawableTint="@android:color/white" android:layout_width="124dp" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/bottomOfTop" app:layout_constraintStart_toEndOf="@id/mainButton" + app:layout_constraintEnd_toEndOf="@id/resetButton" + android:paddingEnd="4dp" + /> + + <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" + android:id="@+id/resetButton" + android:layout_width="124dp" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/bottomOfTop" + app:layout_constraintStart_toEndOf="@id/altButton" app:layout_constraintEnd_toEndOf="parent" android:paddingEnd="4dp" /> diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index 763930db1d55..07a40c86d03a 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -33,9 +33,4 @@ <!-- Whether the user switcher chip shows in the status bar. When true, the multi user avatar will no longer show on the lockscreen --> <bool name="flag_user_switcher_chip">false</bool> - - <!-- Whether the battery icon is allowed to display a shield when battery life is being - protected. --> - <bool name="flag_battery_shield_icon">false</bool> - </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 8322b6c85aed..acc12d7fdaf0 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1431,10 +1431,10 @@ <string name="no_unseen_notif_text">No new notifications</string> <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=50] --> - <string name="adaptive_notification_edu_hun_title">Adaptive notifications is on</string> + <string name="adaptive_notification_edu_hun_title">Notification cooldown is on</string> <!-- Text of heads up notification for adaptive notifications user education. [CHAR LIMIT=100] --> - <string name="adaptive_notification_edu_hun_text">Your device now lowers the volume and reduces pop-ups on the screen for up to two minutes when you receive many notifications in a short time span.</string> + <string name="adaptive_notification_edu_hun_text">Your device volume and alerts are reduced automatically for up to 2 minutes when you get too many notifications at once.</string> <!-- Action label for going to adaptive notification settings [CHAR LIMIT=20] --> <string name="go_to_adaptive_notification_settings">Turn off</string> @@ -3677,4 +3677,31 @@ <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string> <!-- Label for volume undo action [CHAR LIMIT=NONE] --> <string name="volume_undo_action">Undo</string> + + <!-- Keyboard touchpad contextual education strings--> + <!-- Education toast text for Back [CHAR_LIMIT=100] --> + <string name="back_edu_toast_content">To go back, swipe left or right with three fingers on the touchpad</string> + <!-- Education toast text for Home [CHAR_LIMIT=100] --> + <string name="home_edu_toast_content">To go home, swipe up with three fingers on the touchpad</string> + <!-- Education toast text for Overview [CHAR_LIMIT=100] --> + <string name="overview_edu_toast_content">To view recent apps, swipe up and hold with three fingers on the touchpad</string> + <!-- Education toast text for All Apps [CHAR_LIMIT=100] --> + <string name="all_apps_edu_toast_content">To view all your apps, press the action key on your keyboard</string> + + <!-- Education notification title for Back [CHAR_LIMIT=100] --> + <string name="back_edu_notification_title">Use your touchpad to go back</string> + <!-- Education notification text for Back [CHAR_LIMIT=100] --> + <string name="back_edu_notification_content">Swipe left or right using three fingers. Tap to learn more gestures.</string> + <!-- Education notification title for Home [CHAR_LIMIT=100] --> + <string name="home_edu_notification_title">Use your touchpad to go home</string> + <!-- Education notification text for Home [CHAR_LIMIT=100] --> + <string name="home_edu_notification_content">Swipe up using three fingers. Tap to learn more gestures.</string> + <!-- Education notification title for Overview [CHAR_LIMIT=100] --> + <string name="overview_edu_notification_title">Use your touchpad to view recent apps</string> + <!-- Education notification text for Overview [CHAR_LIMIT=100] --> + <string name="overview_edu_notification_content">Swipe up and hold using three fingers. Tap to learn more gestures.</string> + <!-- Education notification title for All Apps [CHAR_LIMIT=100] --> + <string name="all_apps_edu_notification_title">Use your keyboard to view all apps</string> + <!-- Education notification text for All Apps [CHAR_LIMIT=100] --> + <string name="all_apps_edu_notification_content">Press the action key at any time. Tap to learn more gestures.</string> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 845ca5e8b9ec..3019fe796d7d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -344,7 +344,7 @@ public class ActivityManagerWrapper { * Shows a voice session identified by {@code token} * @return true if the session was shown, false otherwise */ - public boolean showVoiceSession(@NonNull IBinder token, @NonNull Bundle args, int flags, + public boolean showVoiceSession(IBinder token, @NonNull Bundle args, int flags, @Nullable String attributionTag) { IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface( ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 484e758bf973..4ef1f93481f7 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -124,6 +124,8 @@ public class QuickStepContract { public static final long SYSUI_STATE_SHORTCUT_HELPER_SHOWING = 1L << 32; // Touchpad gestures are disabled public static final long SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED = 1L << 33; + // PiP animation is running + public static final long SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING = 1L << 34; // Communal hub is showing public static final long SYSUI_STATE_COMMUNAL_HUB_SHOWING = 1L << 35; @@ -175,6 +177,7 @@ public class QuickStepContract { SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY, SYSUI_STATE_SHORTCUT_HELPER_SHOWING, SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED, + SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, SYSUI_STATE_COMMUNAL_HUB_SHOWING, }) public @interface SystemUiStateFlags {} @@ -280,6 +283,9 @@ public class QuickStepContract { if ((flags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) != 0) { str.add("touchpad_gestures_disabled"); } + if ((flags & SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING) != 0) { + str.add("disable_gesture_pip_animating"); + } if ((flags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { str.add("communal_hub_showing"); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java index 6e5e44e45548..e9c9bc7799a5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java @@ -363,6 +363,16 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks } @MainThread + void updateSettingsButtonStatus(int displayId, + @WindowMagnificationSettings.MagnificationSize int index) { + final MagnificationSettingsController magnificationSettingsController = + mMagnificationSettingsSupplier.get(displayId); + if (magnificationSettingsController != null) { + magnificationSettingsController.updateSettingsButtonStatusOnRestore(index); + } + } + + @MainThread void toggleSettingsPanelVisibility(int displayId) { final MagnificationSettingsController magnificationSettingsController = mMagnificationSettingsSupplier.get(displayId); @@ -446,6 +456,11 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks @VisibleForTesting final WindowMagnifierCallback mWindowMagnifierCallback = new WindowMagnifierCallback() { @Override + public void onWindowMagnifierBoundsRestored(int displayId, int index) { + mHandler.post(() -> updateSettingsButtonStatus(displayId, index)); + } + + @Override public void onWindowMagnifierBoundsChanged(int displayId, Rect frame) { if (mMagnificationConnectionImpl != null) { mMagnificationConnectionImpl.onWindowMagnifierBoundsChanged(displayId, frame); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index ed7062b5a335..caf55174b6b5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -100,6 +100,10 @@ public class MagnificationSettingsController implements ComponentCallbacks { mWindowMagnificationSettings.toggleSettingsPanelVisibility(); } + void updateSettingsButtonStatusOnRestore(@MagnificationSize int index) { + mWindowMagnificationSettings.updateSelectedButton(index); + } + void closeMagnificationSettings() { mContext.unregisterComponentCallbacks(this); mWindowMagnificationSettings.hideSettingPanel(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index b37ba8994e44..3828f9f66857 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -127,6 +127,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private final WindowManager mWm; private float mScale; + private int mSettingsButtonIndex = MagnificationSize.DEFAULT; /** * MagnificationFrame represents the bound of {@link #mMirrorSurfaceView} and is constrained @@ -436,6 +437,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (!mMagnificationSizeScaleOptions.contains(index)) { return; } + mSettingsButtonIndex = index; int size = getMagnificationWindowSizeFromIndex(index); setWindowSize(size, size); } @@ -446,6 +448,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return (int) (initSize * scale) - (int) (initSize * scale) % 2; } + int getMagnificationFrameSizeFromIndex(@MagnificationSize int index) { + return getMagnificationWindowSizeFromIndex(index) - 2 * mMirrorSurfaceMargin; + } + void setEditMagnifierSizeMode(boolean enable) { mEditSizeEnable = enable; applyResourcesValues(); @@ -457,8 +463,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (!enable) { // Keep the magnifier size when exiting edit mode - mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity( + mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity( + mSettingsButtonIndex, new Size(mMagnificationFrame.width(), mMagnificationFrame.height())); + } else { + mSettingsButtonIndex = MagnificationSize.CUSTOM; } } @@ -944,7 +953,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void setMagnificationFrame(int width, int height, int centerX, int centerY) { - mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(new Size(width, height)); + mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity( + mSettingsButtonIndex, new Size(width, height)); // Sets the initial frame area for the mirror and place it to the given center on the // display. @@ -954,6 +964,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private Size restoreMagnificationWindowFrameSizeIfPossible() { + if (Flags.saveAndRestoreMagnificationSettingsButtons()) { + return restoreMagnificationWindowFrameIndexAndSizeIfPossible(); + } + if (!mWindowMagnificationFrameSizePrefs.isPreferenceSavedForCurrentDensity()) { return getDefaultMagnificationWindowFrameSize(); } @@ -961,8 +975,37 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity(); } + private Size restoreMagnificationWindowFrameIndexAndSizeIfPossible() { + if (!mWindowMagnificationFrameSizePrefs.isPreferenceSavedForCurrentDensity()) { + notifyWindowSizeRestored(MagnificationSize.DEFAULT); + return getDefaultMagnificationWindowFrameSize(); + } + + // This will return DEFAULT index if the stored preference is in an invalid format. + // Therefore, except CUSTOM, we would like to calculate the window width and height based + // on the restored MagnificationSize index. + int restoredIndex = mWindowMagnificationFrameSizePrefs.getIndexForCurrentDensity(); + notifyWindowSizeRestored(restoredIndex); + if (restoredIndex == MagnificationSize.CUSTOM) { + return mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity(); + } + + int restoredSize = getMagnificationFrameSizeFromIndex(restoredIndex); + return new Size(restoredSize, restoredSize); + } + + private void notifyWindowSizeRestored(@MagnificationSize int index) { + mSettingsButtonIndex = index; + if (isActivated()) { + // Send the callback only if the window magnification is activated. The check is to + // avoid updating the settings panel in the cases that window magnification is not yet + // activated such as during the constructor initialization of this class. + mWindowMagnifierCallback.onWindowMagnifierBoundsRestored(mDisplayId, index); + } + } + private Size getDefaultMagnificationWindowFrameSize() { - final int defaultSize = getMagnificationWindowSizeFromIndex(MagnificationSize.MEDIUM) + final int defaultSize = getMagnificationWindowSizeFromIndex(MagnificationSize.DEFAULT) - 2 * mMirrorSurfaceMargin; return new Size(defaultSize, defaultSize); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java index e83e85e1af1a..ee36c6e8ad35 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java @@ -16,10 +16,14 @@ package com.android.systemui.accessibility; +import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; + import android.content.Context; import android.content.SharedPreferences; import android.util.Size; +import com.android.systemui.Flags; + /** * Class to handle SharedPreference for window magnification size. */ @@ -47,9 +51,15 @@ final class WindowMagnificationFrameSizePrefs { /** * Saves the window frame size for current screen density. */ - public void saveSizeForCurrentDensity(Size size) { - mWindowMagnificationSizePreferences.edit() - .putString(getKey(), size.toString()).apply(); + public void saveIndexAndSizeForCurrentDensity(int index, Size size) { + if (Flags.saveAndRestoreMagnificationSettingsButtons()) { + mWindowMagnificationSizePreferences.edit() + .putString(getKey(), + WindowMagnificationFrameSpec.serialize(index, size)).apply(); + } else { + mWindowMagnificationSizePreferences.edit() + .putString(getKey(), size.toString()).apply(); + } } /** @@ -62,10 +72,32 @@ final class WindowMagnificationFrameSizePrefs { } /** + * Gets the index preference for current screen density. Returns DEFAULT if no preference + * is found. + */ + public @MagnificationSize int getIndexForCurrentDensity() { + final String spec = mWindowMagnificationSizePreferences.getString(getKey(), null); + if (spec == null) { + return MagnificationSize.DEFAULT; + } + try { + return WindowMagnificationFrameSpec.deserialize(spec).getIndex(); + } catch (NumberFormatException e) { + return MagnificationSize.DEFAULT; + } + } + + /** * Gets the size preference for current screen density. */ public Size getSizeForCurrentDensity() { - return Size.parseSize(mWindowMagnificationSizePreferences.getString(getKey(), null)); + if (Flags.saveAndRestoreMagnificationSettingsButtons()) { + return WindowMagnificationFrameSpec + .deserialize(mWindowMagnificationSizePreferences.getString(getKey(), null)) + .getSize(); + } else { + return Size.parseSize(mWindowMagnificationSizePreferences.getString(getKey(), null)); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSpec.kt b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSpec.kt new file mode 100644 index 000000000000..c261a997aec5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSpec.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.systemui.accessibility + +import android.util.Size + +data class WindowMagnificationFrameSpec(val index: Int, val size: Size) { + + companion object { + private fun throwInvalidWindowMagnificationFrameSpec(s: String?): Nothing { + throw NumberFormatException("Invalid WindowMagnificationFrameSpec: \"$s\"") + } + + @JvmStatic fun serialize(index: Int, size: Size) = "$index,$size" + + @JvmStatic + fun deserialize(s: String): WindowMagnificationFrameSpec { + val separatorIndex = s.indexOf(',') + if (separatorIndex < 0) { + throwInvalidWindowMagnificationFrameSpec(s) + } + return try { + WindowMagnificationFrameSpec( + s.substring(0, separatorIndex).toInt(), + Size.parseSize(s.substring(separatorIndex + 1)) + ) + } catch (e: NumberFormatException) { + throwInvalidWindowMagnificationFrameSpec(s) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 5f6f21a6845b..99d966dfd9aa 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -58,6 +58,7 @@ import android.widget.Switch; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView; import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; @@ -98,7 +99,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private Button mDoneButton; private Button mEditButton; private ImageButton mFullScreenButton; - private int mLastSelectedButtonIndex = MagnificationSize.NONE; + private int mLastSelectedButtonIndex = MagnificationSize.DEFAULT; private boolean mAllowDiagonalScrolling = false; @@ -115,19 +116,21 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Retention(RetentionPolicy.SOURCE) @IntDef({ - MagnificationSize.NONE, + MagnificationSize.CUSTOM, MagnificationSize.SMALL, MagnificationSize.MEDIUM, MagnificationSize.LARGE, - MagnificationSize.FULLSCREEN + MagnificationSize.FULLSCREEN, + MagnificationSize.DEFAULT }) /** Denotes the Magnification size type. */ public @interface MagnificationSize { - int NONE = 0; + int CUSTOM = 0; int SMALL = 1; int MEDIUM = 2; int LARGE = 3; int FULLSCREEN = 4; + int DEFAULT = MEDIUM; } @VisibleForTesting @@ -445,13 +448,20 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private void updateUIControlsIfNeeded() { int capability = getMagnificationCapability(); int selectedButtonIndex = mLastSelectedButtonIndex; + WindowMagnificationFrameSizePrefs windowMagnificationFrameSizePrefs = + new WindowMagnificationFrameSizePrefs(mContext); switch (capability) { case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW: mEditButton.setVisibility(View.VISIBLE); mAllowDiagonalScrollingView.setVisibility(View.VISIBLE); mFullScreenButton.setVisibility(View.GONE); if (selectedButtonIndex == MagnificationSize.FULLSCREEN) { - selectedButtonIndex = MagnificationSize.NONE; + if (Flags.saveAndRestoreMagnificationSettingsButtons()) { + selectedButtonIndex = + windowMagnificationFrameSizePrefs.getIndexForCurrentDensity(); + } else { + selectedButtonIndex = MagnificationSize.CUSTOM; + } } break; @@ -613,7 +623,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest public void editMagnifierSizeMode(boolean enable) { setEditMagnifierSizeMode(enable); - updateSelectedButton(MagnificationSize.NONE); + updateSelectedButton(MagnificationSize.CUSTOM); hideSettingPanel(); } @@ -621,7 +631,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest if (index == MagnificationSize.FULLSCREEN) { // transit to fullscreen magnifier if needed transitToMagnificationMode(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); - } else if (index != MagnificationSize.NONE) { + } else if (index != MagnificationSize.CUSTOM) { // update the window magnifier size mCallback.onSetMagnifierSize(index); // transit to window magnifier if needed @@ -706,7 +716,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest }); } - private void updateSelectedButton(@MagnificationSize int index) { + void updateSelectedButton(@MagnificationSize int index) { // Clear the state of last selected button if (mLastSelectedButtonIndex == MagnificationSize.SMALL) { mSmallButton.setSelected(false); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java index a25e9a20f81c..b4a248239f46 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility; +import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; + import android.graphics.Rect; /** @@ -68,4 +70,9 @@ interface WindowMagnifierCallback { * @param displayId The logical display id. */ void onClickSettingsButton(int displayId); + + /** + * Called when restoring the magnification window size. + */ + void onWindowMagnifierBoundsRestored(int displayId, @MagnificationSize int index); } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java index f1fb45c9b16e..baca9594dd2f 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java @@ -79,7 +79,8 @@ public class ShadeTouchHandler implements TouchHandler { if (mCapture != null && mCapture) { sendTouchEvent((MotionEvent) ev); } - if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { + if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP + || ((MotionEvent) ev).getAction() == MotionEvent.ACTION_CANCEL) { session.pop(); } } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java index 4035e957a2db..efa55e90081e 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java @@ -128,8 +128,13 @@ public class TouchMonitor { completer.set(predecessor); } - if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { - stopMonitoring(false); + if (mActiveTouchSessions.isEmpty()) { + if (mStopMonitoringPending) { + stopMonitoring(false); + } else { + // restart monitoring to reset any destructive state on the input session + startMonitoring(); + } } }); diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java index 5c53234f74eb..e63472684585 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java @@ -88,7 +88,6 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { private boolean mPowerSaveEnabled; private boolean mIsBatteryDefender; private boolean mIsIncompatibleCharging; - private boolean mDisplayShieldEnabled; // Error state where we know nothing about the current battery state private boolean mBatteryStateUnknown; // Lazily-loaded since this is expected to be a rare-if-ever state @@ -270,7 +269,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { int resId = 0; if (mPowerSaveEnabled) { resId = R.drawable.battery_unified_attr_powersave; - } else if (mIsBatteryDefender && mDisplayShieldEnabled) { + } else if (mIsBatteryDefender) { resId = R.drawable.battery_unified_attr_defend; } else if (isCharging) { resId = R.drawable.battery_unified_attr_charging; @@ -288,7 +287,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { private ColorProfile getCurrentColorProfile() { return getColorProfile( mPowerSaveEnabled, - mIsBatteryDefender && mDisplayShieldEnabled, + mIsBatteryDefender, mPluggedIn, mLevel <= 20); } @@ -410,10 +409,6 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { mBatteryEstimateFetcher = fetcher; } - void setDisplayShieldEnabled(boolean displayShieldEnabled) { - mDisplayShieldEnabled = displayShieldEnabled; - } - void updatePercentText() { if (!newStatusBarIcons()) { updatePercentTextLegacy(); @@ -659,7 +654,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { float mainBatteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor; - boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender; + boolean displayShield = mIsBatteryDefender; float fullBatteryIconHeight = BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield); float fullBatteryIconWidth = diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java index 4f13e6ffd0b5..9a30c213a2f9 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java @@ -34,7 +34,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; @@ -153,8 +152,6 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> mBatteryController = batteryController; mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString); - mView.setDisplayShieldEnabled( - getContext().getResources().getBoolean(R.bool.flag_battery_shield_icon)); mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery); mSettingObserver = new SettingObserver(mMainHandler); diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt index 85e2bdb43ba5..b6ace81d18ba 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt @@ -19,10 +19,14 @@ package com.android.systemui.common.ui.view import android.annotation.SuppressLint import android.content.Context +import android.os.Bundle import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import com.android.systemui.shade.TouchLogger import kotlin.math.pow import kotlin.math.sqrt @@ -44,6 +48,10 @@ class LongPressHandlingView( attrs, ) { + init { + setupAccessibilityDelegate() + } + constructor( context: Context, attrs: AttributeSet?, @@ -55,6 +63,7 @@ class LongPressHandlingView( view: View, x: Int, y: Int, + isA11yAction: Boolean = false, ) /** Notifies that the gesture was too short for a long press, it is actually a click. */ @@ -63,6 +72,8 @@ class LongPressHandlingView( var listener: Listener? = null + var accessibilityHintLongPressAction: AccessibilityAction? = null + private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy { LongPressHandlingViewInteractionHandler( postDelayed = { block, timeoutMs -> @@ -107,6 +118,51 @@ class LongPressHandlingView( override fun onTouchEvent(event: MotionEvent?): Boolean { return interactionHandler.onTouchEvent(event?.toModel()) } + + private fun setupAccessibilityDelegate() { + accessibilityDelegate = + object : AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + v: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(v, info) + if ( + interactionHandler.isLongPressHandlingEnabled && + accessibilityHintLongPressAction != null + ) { + info.addAction(accessibilityHintLongPressAction) + } + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + return if ( + interactionHandler.isLongPressHandlingEnabled && + action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK + ) { + val longPressHandlingView = host as? LongPressHandlingView + if (longPressHandlingView != null) { + // the coordinates are not available as it is an a11y long press + listener?.onLongPressDetected( + view = longPressHandlingView, + x = 0, + y = 0, + isA11yAction = true, + ) + true + } else { + false + } + } else { + super.performAccessibilityAction(host, action, args) + } + } + } + } } private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel { diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 3d201a36417b..a44533555a04 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.dagger import android.content.Context +import com.android.systemui.CoreStartable import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule @@ -26,6 +27,7 @@ import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryM import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule +import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.CommunalColorsImpl @@ -40,6 +42,8 @@ import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import kotlinx.coroutines.CoroutineScope @Module( @@ -69,6 +73,13 @@ interface CommunalModule { @Binds fun bindCommunalColors(impl: CommunalColorsImpl): CommunalColors + @Binds + @IntoMap + @ClassKey(CommunalSceneTransitionInteractor::class) + abstract fun bindCommunalSceneTransitionInteractor( + impl: CommunalSceneTransitionInteractor + ): CoreStartable + companion object { @Provides @Communal diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt index d8067b887c67..4de39c457f3b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -49,14 +49,8 @@ interface CommunalPrefsRepository { /** Whether the CTA tile has been dismissed. */ fun isCtaDismissed(user: UserInfo): Flow<Boolean> - /** Whether the lock screen widget disclaimer has been dismissed by the user. */ - fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> - /** Save the CTA tile dismissed state for the current user. */ suspend fun setCtaDismissed(user: UserInfo) - - /** Save the lock screen widget disclaimer dismissed state for the current user. */ - suspend fun setDisclaimerDismissed(user: UserInfo) } @OptIn(ExperimentalCoroutinesApi::class) @@ -74,9 +68,6 @@ constructor( override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = readKeyForUser(user, CTA_DISMISSED_STATE) - override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> = - readKeyForUser(user, DISCLAIMER_DISMISSED_STATE) - /** * Emits an event each time a Backup & Restore restoration job is completed, and once at the * start of collection. @@ -97,12 +88,6 @@ constructor( logger.i("Dismissed CTA tile") } - override suspend fun setDisclaimerDismissed(user: UserInfo) = - withContext(bgDispatcher) { - getSharedPrefsForUser(user).edit().putBoolean(DISCLAIMER_DISMISSED_STATE, true).apply() - logger.i("Dismissed widget disclaimer") - } - private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { return userFileManager.getSharedPreferences( FILE_NAME, @@ -124,6 +109,5 @@ constructor( const val TAG = "CommunalPrefsRepository" const val FILE_NAME = "communal_hub_prefs" const val CTA_DISMISSED_STATE = "cta_dismissed" - const val DISCLAIMER_DISMISSED_STATE = "disclaimer_dismissed" } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index 7a4006d515f7..260dcbad6201 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -28,7 +28,6 @@ import com.android.systemui.scene.shared.model.SceneDataSource import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -52,7 +51,7 @@ interface CommunalSceneRepository { fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) /** Immediately snaps to the desired scene. */ - fun snapToScene(toScene: SceneKey, delayMillis: Long = 0) + fun snapToScene(toScene: SceneKey) /** * Updates the transition state of the hub [SceneTransitionLayout]. @@ -93,11 +92,10 @@ constructor( } } - override fun snapToScene(toScene: SceneKey, delayMillis: Long) { + override fun snapToScene(toScene: SceneKey) { applicationScope.launch { // SceneTransitionLayout state updates must be triggered on the thread the STL was // created on. - delay(delayMillis) sceneDataSource.snapToScene(toScene) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt new file mode 100644 index 000000000000..7d9e1df41e6f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.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.systemui.communal.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow + +@SysUISingleton +class CommunalSceneTransitionRepository @Inject constructor() { + /** + * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the + * next transition away from communal scene is started. It will be consumed exactly once and + * after that the state will be set back to null. + */ + val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null) +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 3fffd76ab6a9..dbddc23d6146 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -23,6 +23,7 @@ import android.content.pm.UserInfo import android.os.UserHandle import android.os.UserManager import android.provider.Settings +import com.android.app.tracing.coroutines.launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -64,10 +65,12 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject +import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -94,6 +97,7 @@ class CommunalInteractor @Inject constructor( @Application val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, @Background val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, private val widgetRepository: CommunalWidgetRepository, @@ -117,9 +121,25 @@ constructor( private val _editModeOpen = MutableStateFlow(false) - /** Whether edit mode is currently open. */ + /** + * Whether edit mode is currently open. This will be true from onCreate to onDestroy in + * [EditWidgetsActivity] and thus does not correspond to whether or not the activity is visible. + * + * Note that since this is called in onDestroy, it's not guaranteed to ever be set to false when + * edit mode is closed, such as in the case that a user exits edit mode manually with a back + * gesture or navigation gesture. + */ val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow() + private val _editActivityShowing = MutableStateFlow(false) + + /** + * Whether the edit mode activity is currently showing. This is true from onStart to onStop in + * [EditWidgetsActivity] so may be false even when the user is in edit mode, such as when a + * widget's individual configuration activity has launched. + */ + val editActivityShowing: StateFlow<Boolean> = _editActivityShowing.asStateFlow() + /** Whether communal features are enabled. */ val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled @@ -148,6 +168,17 @@ constructor( replay = 1, ) + private val _isDisclaimerDismissed = MutableStateFlow(false) + val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() + + fun setDisclaimerDismissed() { + bgScope.launch("$TAG#setDisclaimerDismissed") { + _isDisclaimerDismissed.value = true + delay(DISCLAIMER_RESET_MILLIS) + _isDisclaimerDismissed.value = false + } + } + /** Whether to show communal when exiting the occluded state. */ val showCommunalFromOccluded: Flow<Boolean> = keyguardTransitionInteractor.startedKeyguardTransitionStep @@ -301,6 +332,10 @@ constructor( _editModeOpen.value = isOpen } + fun setEditActivityShowing(isOpen: Boolean) { + _editActivityShowing.value = isOpen + } + /** Show the widget editor Activity. */ fun showWidgetEditor( preselectedKey: String? = null, @@ -510,6 +545,14 @@ constructor( } companion object { + const val TAG = "CommunalInteractor" + + /** + * The amount of time between showing the widget disclaimer to the user as measured from the + * moment the disclaimer is dimsissed. + */ + val DISCLAIMER_RESET_MILLIS = 30.minutes + /** * The user activity timeout which should be used when the communal hub is opened. A value * of -1 means that the user's chosen screen timeout will be used instead. diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt index 3517650c8cf3..0b5f40d8041e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt @@ -17,7 +17,6 @@ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo -import com.android.app.tracing.coroutines.launch import com.android.systemui.communal.data.repository.CommunalPrefsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -43,7 +42,7 @@ constructor( private val repository: CommunalPrefsRepository, userInteractor: SelectedUserInteractor, private val userTracker: UserTracker, - @CommunalTableLog tableLogBuffer: TableLogBuffer + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { val isCtaDismissed: Flow<Boolean> = @@ -64,25 +63,6 @@ constructor( suspend fun setCtaDismissed(user: UserInfo = userTracker.userInfo) = repository.setCtaDismissed(user) - val isDisclaimerDismissed: Flow<Boolean> = - userInteractor.selectedUserInfo - .flatMapLatest { user -> repository.isDisclaimerDismissed(user) } - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - columnPrefix = "", - columnName = "isDisclaimerDismissed", - initialValue = false, - ) - .stateIn( - scope = bgScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - - fun setDisclaimerDismissed(user: UserInfo = userTracker.userInfo) { - bgScope.launch("$TAG#setDisclaimerDismissed") { repository.setDisclaimerDismissed(user) } - } - private companion object { const val TAG = "CommunalPrefsInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index 122f964713a9..45cfe3673f3d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import com.android.app.tracing.coroutines.launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -26,9 +27,11 @@ import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.model.KeyguardState import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -39,6 +42,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @@ -48,7 +52,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val communalSceneRepository: CommunalSceneRepository, ) { - val _isLaunchingWidget = MutableStateFlow(false) + private val _isLaunchingWidget = MutableStateFlow(false) /** Whether a widget launch is currently in progress. */ val isLaunchingWidget: StateFlow<Boolean> = _isLaunchingWidget.asStateFlow() @@ -57,17 +61,48 @@ constructor( _isLaunchingWidget.value = launching } + fun interface OnSceneAboutToChangeListener { + /** Notifies that the scene is about to change to [toScene]. */ + fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) + } + + private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>() + + /** Registers a listener which is called when the scene is about to change. */ + fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) { + onSceneAboutToChangeListener.add(processor) + } + /** * Asks for an asynchronous scene witch to [newScene], which will use the corresponding * installed transition or the one specified by [transitionKey], if provided. */ - fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) { - communalSceneRepository.changeScene(newScene, transitionKey) + fun changeScene( + newScene: SceneKey, + transitionKey: TransitionKey? = null, + keyguardState: KeyguardState? = null, + ) { + applicationScope.launch { + notifyListeners(newScene, keyguardState) + communalSceneRepository.changeScene(newScene, transitionKey) + } } /** Immediately snaps to the new scene. */ - fun snapToScene(newScene: SceneKey, delayMillis: Long = 0) { - communalSceneRepository.snapToScene(newScene, delayMillis) + fun snapToScene( + newScene: SceneKey, + delayMillis: Long = 0, + keyguardState: KeyguardState? = null + ) { + applicationScope.launch("$TAG#snapToScene") { + delay(delayMillis) + notifyListeners(newScene, keyguardState) + communalSceneRepository.snapToScene(newScene) + } + } + + private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) { + onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) } } /** Changes to Blank scene when starting an activity after dismissing keyguard. */ @@ -164,4 +199,8 @@ constructor( started = SharingStarted.WhileSubscribed(), initialValue = false, ) + + private companion object { + const val TAG = "CommunalSceneInteractor" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt new file mode 100644 index 000000000000..8351566fcae6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -0,0 +1,293 @@ +/* + * 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.systemui.communal.domain.interactor + +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.communalSceneKtfRefactor +import com.android.systemui.communal.data.repository.CommunalSceneTransitionRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.InternalKeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.util.kotlin.pairwise +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * This class listens to [SceneTransitionLayout] transitions and manages keyguard transition + * framework (KTF) states accordingly for communal states. + * + * There are a few rules: + * - There are only 2 communal scenes: [CommunalScenes.Communal] and [CommunalScenes.Blank] + * - When scene framework is on [CommunalScenes.Blank], KTF is allowed to change its scenes freely + * - When scene framework is on [CommunalScenes.Communal], KTF is locked into + * [KeyguardState.GLANCEABLE_HUB] + */ +@SysUISingleton +class CommunalSceneTransitionInteractor +@Inject +constructor( + val transitionInteractor: KeyguardTransitionInteractor, + val internalTransitionInteractor: InternalKeyguardTransitionInteractor, + private val settingsInteractor: CommunalSettingsInteractor, + @Application private val applicationScope: CoroutineScope, + private val sceneInteractor: CommunalSceneInteractor, + private val repository: CommunalSceneTransitionRepository, + keyguardInteractor: KeyguardInteractor, +) : CoreStartable, CommunalSceneInteractor.OnSceneAboutToChangeListener { + + private var currentTransitionId: UUID? = null + private var progressJob: Job? = null + + private val currentToState: KeyguardState + get() = internalTransitionInteractor.currentTransitionInfoInternal.value.to + + /** + * The next keyguard state to trigger when exiting [CommunalScenes.Communal]. This is only used + * if the state is changed by user gesture or not explicitly defined by the caller when changing + * scenes programmatically. + * + * This is needed because we do not always want to exit back to the KTF state we came from. For + * example, when going from HUB (Communal) -> OCCLUDED (Blank) -> HUB (Communal) and then + * closing the hub via gesture, we don't want to go back to OCCLUDED but instead either go to + * DREAM or LOCKSCREEN depending on if there is a dream showing. + */ + private val nextKeyguardStateInternal = + combine( + keyguardInteractor.isDreaming, + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.isKeyguardGoingAway, + ) { dreaming, occluded, keyguardGoingAway -> + if (keyguardGoingAway) { + KeyguardState.GONE + } else if (dreaming) { + KeyguardState.DREAMING + } else if (occluded) { + KeyguardState.OCCLUDED + } else { + KeyguardState.LOCKSCREEN + } + } + + private val nextKeyguardState: StateFlow<KeyguardState> = + combine( + repository.nextLockscreenTargetState, + nextKeyguardStateInternal, + ) { override, nextState -> + override ?: nextState + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = KeyguardState.LOCKSCREEN, + ) + + override fun start() { + if ( + communalSceneKtfRefactor() && + settingsInteractor.isCommunalFlagEnabled() && + !SceneContainerFlag.isEnabled + ) { + sceneInteractor.registerSceneStateProcessor(this) + listenForSceneTransitionProgress() + } + } + + /** + * Called when the scene is programmatically changed, allowing callers to specify which KTF + * state should be set when transitioning to [CommunalScenes.Blank] + */ + override fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) { + if (toScene != CommunalScenes.Blank || keyguardState == null) return + repository.nextLockscreenTargetState.value = keyguardState + } + + /** Monitors [SceneTransitionLayout] state and updates KTF state accordingly. */ + private fun listenForSceneTransitionProgress() { + applicationScope.launch { + sceneInteractor.transitionState + .pairwise(ObservableTransitionState.Idle(CommunalScenes.Blank)) + .collect { (prevTransition, transition) -> + when (transition) { + is ObservableTransitionState.Idle -> handleIdle(prevTransition, transition) + is ObservableTransitionState.Transition -> + handleTransition(prevTransition, transition) + } + } + } + } + + private suspend fun handleIdle( + prevTransition: ObservableTransitionState, + idle: ObservableTransitionState.Idle + ) { + if ( + prevTransition is ObservableTransitionState.Transition && + currentTransitionId != null && + idle.currentScene == prevTransition.toScene + ) { + finishCurrentTransition() + } else { + // We may receive an Idle event without a corresponding Transition + // event, such as when snapping to a scene without an animation. + val targetState = + if (idle.currentScene == CommunalScenes.Blank) { + nextKeyguardState.value + } else { + KeyguardState.GLANCEABLE_HUB + } + transitionKtfTo(targetState) + repository.nextLockscreenTargetState.value = null + } + } + + private fun finishCurrentTransition() { + internalTransitionInteractor.updateTransition( + currentTransitionId!!, + 1f, + TransitionState.FINISHED + ) + resetTransitionData() + } + + private suspend fun finishReversedTransitionTo(state: KeyguardState) { + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = internalTransitionInteractor.currentTransitionInfoInternal.value.to, + to = state, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.REVERSE + ) + currentTransitionId = internalTransitionInteractor.startTransition(newTransition) + internalTransitionInteractor.updateTransition( + currentTransitionId!!, + 1f, + TransitionState.FINISHED + ) + resetTransitionData() + } + + private fun resetTransitionData() { + progressJob?.cancel() + progressJob = null + currentTransitionId = null + } + + private suspend fun handleTransition( + prevTransition: ObservableTransitionState, + transition: ObservableTransitionState.Transition + ) { + if (prevTransition.isTransitioning(from = transition.fromScene, to = transition.toScene)) { + // This is a new transition, but exactly the same as the previous state. Skip resetting + // KTF for this case and just collect the new progress instead. + collectProgress(transition) + } else if (transition.toScene == CommunalScenes.Communal) { + if (currentTransitionId != null) { + if (currentToState == KeyguardState.GLANCEABLE_HUB) { + transitionKtfTo(transitionInteractor.getStartedFromState()) + } + } + startTransitionToGlanceableHub() + collectProgress(transition) + } else if (transition.toScene == CommunalScenes.Blank) { + if (currentTransitionId != null) { + // Another transition started before this one is completed. Transition to the + // GLANCEABLE_HUB state so that we can properly transition away from it. + transitionKtfTo(KeyguardState.GLANCEABLE_HUB) + } + startTransitionFromGlanceableHub() + collectProgress(transition) + } + } + + private suspend fun transitionKtfTo(state: KeyguardState) { + val currentTransition = transitionInteractor.transitionState.value + if (currentTransition.isFinishedIn(state)) { + // This is already the state we want to be in + resetTransitionData() + } else if (currentTransition.isTransitioning(to = state)) { + finishCurrentTransition() + } else { + finishReversedTransitionTo(state) + } + } + + private fun collectProgress(transition: ObservableTransitionState.Transition) { + progressJob?.cancel() + progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } } + } + + private suspend fun startTransitionFromGlanceableHub() { + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = nextKeyguardState.value, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) + repository.nextLockscreenTargetState.value = null + startTransition(newTransition) + } + + private suspend fun startTransitionToGlanceableHub() { + val currentState = internalTransitionInteractor.currentTransitionInfoInternal.value.to + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = currentState, + to = KeyguardState.GLANCEABLE_HUB, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) + startTransition(newTransition) + } + + private suspend fun startTransition(transitionInfo: TransitionInfo) { + if (currentTransitionId != null) { + resetTransitionData() + } + currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo) + } + + private fun updateProgress(progress: Float) { + if (currentTransitionId == null) return + internalTransitionInteractor.updateTransition( + currentTransitionId!!, + progress.coerceIn(0f, 1f), + TransitionState.RUNNING + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 19d7ceba2310..01ed2b71bea0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.media.controls.ui.view.MediaHost import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -75,8 +76,16 @@ abstract class BaseCommunalViewModel( communalInteractor.signalUserInteraction() } - fun changeScene(scene: SceneKey, transitionKey: TransitionKey? = null) { - communalSceneInteractor.changeScene(scene, transitionKey) + /** + * Asks for an asynchronous scene witch to [newScene], which will use the corresponding + * installed transition or the one specified by [transitionKey], if provided. + */ + fun changeScene( + scene: SceneKey, + transitionKey: TransitionKey? = null, + keyguardState: KeyguardState? = null + ) { + communalSceneInteractor.changeScene(scene, transitionKey, keyguardState) } fun setEditModeState(state: EditModeState?) = communalSceneInteractor.setEditModeState(state) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 7b0aadfdcebd..830f543fd06f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -24,7 +24,6 @@ import android.content.res.Resources import android.util.Log import androidx.activity.result.ActivityResultLauncher import com.android.internal.logging.UiEventLogger -import com.android.systemui.Flags.enableWidgetPickerSizeFilter import com.android.systemui.communal.data.model.CommunalWidgetCategories import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor @@ -82,10 +81,10 @@ constructor( communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING } val showDisclaimer: Flow<Boolean> = - allOf(isCommunalContentVisible, not(communalPrefsInteractor.isDisclaimerDismissed)) + allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed)) fun onDisclaimerDismissed() { - communalPrefsInteractor.setDisclaimerDismissed() + communalInteractor.setDisclaimerDismissed() } /** @@ -176,16 +175,14 @@ constructor( return Intent(Intent.ACTION_PICK).apply { setPackage(packageName) - if (enableWidgetPickerSizeFilter()) { - putExtra( - EXTRA_DESIRED_WIDGET_WIDTH, - resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width) - ) - putExtra( - EXTRA_DESIRED_WIDGET_HEIGHT, - resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height) - ) - } + putExtra( + EXTRA_DESIRED_WIDGET_WIDTH, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width) + ) + putExtra( + EXTRA_DESIRED_WIDGET_HEIGHT, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height) + ) putExtra( AppWidgetManager.EXTRA_CATEGORY_FILTER, CommunalWidgetCategories.defaultCategories @@ -217,6 +214,14 @@ constructor( /** Sets whether edit mode is currently open */ fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) + /** + * Sets whether the edit mode activity is currently showing. + * + * See [CommunalInteractor.editActivityShowing] for more info. + */ + fun setEditActivityShowing(showing: Boolean) = + communalInteractor.setEditActivityShowing(showing) + /** Called when exiting the edit mode, before transitioning back to the communal scene. */ fun cleanupEditModeState() { communalSceneInteractor.setEditModeState(null) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt index 4efaf878f33f..08444623f24d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt @@ -37,13 +37,21 @@ class CommunalTransitionAnimatorController( delegate.onIntentStarted(willAnimate) } + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationStart(isExpandingFullyAbove) + // TODO(b/330672236): move this to onTransitionAnimationEnd() without the delay. + communalSceneInteractor.snapToScene( + CommunalScenes.Blank, + ActivityTransitionAnimator.TIMINGS.totalDuration + ) + } + override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { communalSceneInteractor.setIsLaunchingWidget(false) delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) } override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { - communalSceneInteractor.snapToScene(CommunalScenes.Blank) communalSceneInteractor.setIsLaunchingWidget(false) delegate.onTransitionAnimationEnd(isExpandingFullyAbove) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 46f802fd2bce..08fe42ede5d3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -96,8 +96,7 @@ constructor( run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } } } - } - ?: run { Log.w(TAG, "No data in result.") } + } ?: run { Log.w(TAG, "No data in result.") } } else -> Log.w( @@ -195,6 +194,8 @@ constructor( override fun onStart() { super.onStart() + communalViewModel.setEditActivityShowing(true) + if (shouldOpenWidgetPickerOnStart) { onOpenWidgetPicker() shouldOpenWidgetPickerOnStart = false @@ -206,6 +207,7 @@ constructor( override fun onStop() { super.onStop() + communalViewModel.setEditActivityShowing(false) logger.i("Stopping the communal widget editor activity") uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt index 7f8103e63684..6864f4e96994 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt @@ -18,7 +18,7 @@ package com.android.systemui.controls.ui import android.app.Activity import android.app.ActivityOptions -import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED +import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS import android.app.Dialog import android.app.PendingIntent import android.content.ComponentName @@ -93,8 +93,8 @@ class DetailDialog( 0 /* enterResId */, 0 /* exitResId */ ).apply { - pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + pendingIntentBackgroundActivityStartMode = + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS taskAlwaysOnTop = true } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index 3294c816d67c..b45ebd865c55 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -256,6 +256,7 @@ constructor( it.cancel() null } + mOverlayStateController.setExitAnimationsRunning(false) } private fun blurAnimator( diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt index 53b9261991e0..0e2e2e6277f2 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -16,12 +16,21 @@ package com.android.systemui.education.dagger +import com.android.systemui.CoreStartable +import com.android.systemui.Flags import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl +import com.android.systemui.education.domain.interactor.ContextualEducationInteractor +import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor +import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl +import com.android.systemui.shared.education.GestureType import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import java.time.Clock import javax.inject.Qualifier import kotlinx.coroutines.CoroutineDispatcher @@ -53,5 +62,41 @@ interface ContextualEducationModule { fun provideEduClock(): Clock { return Clock.systemUTC() } + + @Provides + @IntoMap + @ClassKey(ContextualEducationInteractor::class) + fun provideContextualEducationInteractor( + implLazy: Lazy<ContextualEducationInteractor> + ): CoreStartable { + return if (Flags.keyboardTouchpadContextualEducation()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + return NoOpCoreStartable + } + } + + @Provides + fun provideKeyboardTouchpadEduStatsInteractor( + implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl> + ): KeyboardTouchpadEduStatsInteractor { + return if (Flags.keyboardTouchpadContextualEducation()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + return NoOpKeyboardTouchpadEduStatsInteractor + } + } + } + + private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { + override fun incrementSignalCount(gestureType: GestureType) {} + + override fun updateShortcutTriggerTime(gestureType: GestureType) {} + } + + private object NoOpCoreStartable : CoreStartable { + override fun start() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt new file mode 100644 index 000000000000..e2aa9111246b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt @@ -0,0 +1,54 @@ +/* + * 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.systemui.education.domain.interactor + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.education.data.repository.ContextualEducationRepository +import com.android.systemui.shared.education.GestureType +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * Allows updating education data (e.g. signal count, shortcut time) for different gesture types. + * Change user education repository when user is changed. + */ +@SysUISingleton +class ContextualEducationInteractor +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + private val selectedUserInteractor: SelectedUserInteractor, + private val repository: ContextualEducationRepository, +) : CoreStartable { + + override fun start() { + backgroundScope.launch { + selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) } + } + } + + suspend fun incrementSignalCount(gestureType: GestureType) = + repository.incrementSignalCount(gestureType) + + suspend fun updateShortcutTriggerTime(gestureType: GestureType) = + repository.updateShortcutTriggerTime(gestureType) +} diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt new file mode 100644 index 000000000000..643e571d2927 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt @@ -0,0 +1,55 @@ +/* + * 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.systemui.education.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.education.GestureType +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is + * for having a different implementation of interactor when the feature flag is off. + */ +interface KeyboardTouchpadEduStatsInteractor { + fun incrementSignalCount(gestureType: GestureType) + + fun updateShortcutTriggerTime(gestureType: GestureType) +} + +/** Allow update to education data related to keyboard/touchpad. */ +@SysUISingleton +class KeyboardTouchpadEduStatsInteractorImpl +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + private val contextualEducationInteractor: ContextualEducationInteractor +) : KeyboardTouchpadEduStatsInteractor { + + override fun incrementSignalCount(gestureType: GestureType) { + // Todo: check if keyboard/touchpad is connected before update + backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) } + } + + override fun updateShortcutTriggerTime(gestureType: GestureType) { + backgroundScope.launch { + contextualEducationInteractor.updateShortcutTriggerTime(gestureType) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 1ba274ff4e76..0e06117b2693 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -37,6 +37,8 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -55,6 +57,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token + NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3d3584e29def..d0beb7abc25b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -137,12 +137,6 @@ object Flags { // TODO(b/267722622): Tracking Bug @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag("wallpaper_picker_ui_for_aiwp") - /** Whether to allow long-press on the lock screen to directly open wallpaper picker. */ - // TODO(b/277220285): Tracking bug. - @JvmField - val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP = - unreleasedFlag("lock_screen_long_press_directly_opens_wallpaper_picker") - /** Whether page transition animations in the wallpaper picker are enabled */ // TODO(b/291710220): Tracking bug. @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index c44eb471d460..491c73d0ae56 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -16,9 +16,15 @@ package com.android.systemui.haptics.qs +import android.content.ComponentName import android.os.VibrationEffect import android.service.quicksettings.Tile +import android.view.View import androidx.annotation.VisibleForTesting +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.DelegateTransitionAnimatorController +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile @@ -58,6 +64,7 @@ constructor( /** The [QSTile] and [Expandable] used to perform a long-click and click actions */ var qsTile: QSTile? = null var expandable: Expandable? = null + private set /** Haptic effects */ private val durations = @@ -125,8 +132,10 @@ constructor( } fun handleAnimationStart() { - vibrate(longPressHint) - setState(State.RUNNING_FORWARD) + if (state == State.TIMEOUT_WAIT) { + vibrate(longPressHint) + setState(State.RUNNING_FORWARD) + } } /** This function is called both when an animator completes or gets cancelled */ @@ -147,7 +156,10 @@ constructor( setState(getStateForClick()) qsTile?.click(expandable) } - State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE) + State.RUNNING_BACKWARDS_FROM_CANCEL -> { + callback?.onEffectFinishedReversing() + setState(State.IDLE) + } else -> {} } } @@ -222,13 +234,58 @@ constructor( fun resetState() = setState(State.IDLE) + fun createExpandableFromView(view: View) { + expandable = + object : Expandable { + override fun activityTransitionController( + launchCujType: Int?, + cookie: ActivityTransitionAnimator.TransitionCookie?, + component: ComponentName?, + returnCujType: Int?, + ): ActivityTransitionAnimator.Controller? { + val delegatedController = + ActivityTransitionAnimator.Controller.fromView( + view, + launchCujType, + cookie, + component, + returnCujType, + ) + return delegatedController?.let { createTransitionControllerDelegate(it) } + } + + override fun dialogTransitionController( + cuj: DialogCuj?, + ): DialogTransitionAnimator.Controller? = + DialogTransitionAnimator.Controller.fromView(view, cuj) + } + } + + @VisibleForTesting + fun createTransitionControllerDelegate( + controller: ActivityTransitionAnimator.Controller + ): DelegateTransitionAnimatorController { + val delegated = + object : DelegateTransitionAnimatorController(controller) { + override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { + if (state == State.LONG_CLICKED) { + setState(State.RUNNING_BACKWARDS_FROM_CANCEL) + callback?.onReverseAnimator(false) + } + delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) + } + } + return delegated + } + enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */ RUNNING_FORWARD, /* The effect is running normally */ /* The effect was interrupted by an ACTION_UP and is now running backwards */ RUNNING_BACKWARDS_FROM_UP, - /* The effect was interrupted by an ACTION_CANCEL and is now running backwards */ + /* The effect was cancelled by an ACTION_CANCEL or a shade collapse and is now running + backwards */ RUNNING_BACKWARDS_FROM_CANCEL, CLICKED, /* The effect has ended with a click */ LONG_CLICKED, /* The effect has ended with a long-click */ @@ -247,7 +304,7 @@ constructor( fun onStartAnimator() /** Reverse the effect animator */ - fun onReverseAnimator() + fun onReverseAnimator(playHaptics: Boolean = true) /** Cancel the effect animator */ fun onCancelAnimator() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 893835a38c53..e5ccc4a14003 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import android.util.Log import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch import com.android.systemui.dagger.SysUISingleton @@ -118,7 +119,9 @@ constructor( // needed. Also, don't react to wake and unlock events, as we'll be // receiving a call to #dismissAod() shortly when the authentication // completes. - !maybeStartTransitionToOccludedOrInsecureCamera() && + !maybeStartTransitionToOccludedOrInsecureCamera { state, reason -> + startTransitionTo(state, ownerReason = reason) + } && !isWakeAndUnlock(biometricUnlockState.mode) && !primaryBouncerShowing } else { @@ -184,11 +187,7 @@ constructor( .filterRelevantKeyguardStateAnd { isOccluded -> isOccluded } .collect { if (!maybeHandleInsecurePowerGesture()) { - startTransitionTo( - toState = KeyguardState.OCCLUDED, - modeOnCanceled = TransitionModeOnCanceled.RESET, - ownerReason = "isOccluded = true", - ) + Log.i(TAG, "Ignoring change to isOccluded to prevent errant AOD->OCCLUDED") } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 8f50b03eafec..8ef138eeb16a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -20,8 +20,11 @@ import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.DreamManager import com.android.app.animation.Interpolators +import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -166,7 +169,7 @@ constructor( } } else if (occluded) { startTransitionTo(KeyguardState.OCCLUDED) - } else if (isIdleOnCommunal) { + } else if (isIdleOnCommunal && !communalSceneKtfRefactor()) { if (SceneContainerFlag.isEnabled) { // TODO(b/336576536): Check if adaptation for scene framework is needed } else { @@ -183,7 +186,7 @@ constructor( if (SceneContainerFlag.isEnabled) { // TODO(b/336576536): Check if adaptation for scene framework is needed } else { - startTransitionTo(KeyguardState.GLANCEABLE_HUB) + transitionToGlanceableHub() } } else { startTransitionTo(KeyguardState.LOCKSCREEN) @@ -218,7 +221,9 @@ constructor( canWakeDirectlyToGone, primaryBouncerShowing) -> if ( - !maybeStartTransitionToOccludedOrInsecureCamera() && + !maybeStartTransitionToOccludedOrInsecureCamera { state, reason -> + startTransitionTo(state, ownerReason = reason) + } && // Handled by dismissFromDozing(). !isWakeAndUnlock(biometricUnlockState.mode) ) { @@ -242,7 +247,7 @@ constructor( ownerReason = "waking from dozing" ) } - } else if (isIdleOnCommunal) { + } else if (isIdleOnCommunal && !communalSceneKtfRefactor()) { if (SceneContainerFlag.isEnabled) { // TODO(b/336576536): Check if adaptation for scene framework is // needed @@ -264,10 +269,7 @@ constructor( // TODO(b/336576536): Check if adaptation for scene framework is // needed } else { - startTransitionTo( - KeyguardState.GLANCEABLE_HUB, - ownerReason = "waking from dozing" - ) + transitionToGlanceableHub() } } else { startTransitionTo( @@ -280,6 +282,18 @@ constructor( } } + private suspend fun transitionToGlanceableHub() { + if (communalSceneKtfRefactor()) { + communalSceneInteractor.changeScene( + CommunalScenes.Communal, + // Immediately show the hub when transitioning from dozing to hub. + CommunalTransitionKeys.Immediately, + ) + } else { + startTransitionTo(KeyguardState.GLANCEABLE_HUB) + } + } + /** Dismisses keyguard from the DOZING state. */ fun dismissFromDozing() { scope.launch { startTransitionTo(KeyguardState.GONE) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 453401d2aa7d..4c3a75e765b6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch +import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -81,7 +82,9 @@ constructor( listenForDreamingToLockscreenOrGone() listenForDreamingToAodOrDozing() listenForTransitionToCamera(scope, keyguardInteractor) - listenForDreamingToGlanceableHub() + if (!communalSceneKtfRefactor()) { + listenForDreamingToGlanceableHub() + } listenForDreamingToPrimaryBouncer() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 1a7012abdbe7..d81195060071 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -19,7 +19,11 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch +import com.android.systemui.Flags.communalSceneKtfRefactor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -30,6 +34,7 @@ import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.noneOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -50,6 +55,7 @@ constructor( private val glanceableHubTransitions: GlanceableHubTransitions, private val communalSettingsInteractor: CommunalSettingsInteractor, keyguardInteractor: KeyguardInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, override val transitionRepository: KeyguardTransitionRepository, override val internalTransitionInteractor: InternalKeyguardTransitionInteractor, transitionInteractor: KeyguardTransitionInteractor, @@ -72,7 +78,9 @@ constructor( if (!communalSettingsInteractor.isCommunalFlagEnabled()) { return } - listenForHubToLockscreenOrDreaming() + if (!communalSceneKtfRefactor()) { + listenForHubToLockscreenOrDreaming() + } listenForHubToDozing() listenForHubToPrimaryBouncer() listenForHubToAlternateBouncer() @@ -120,7 +128,10 @@ constructor( scope.launch("$TAG#listenForHubToPrimaryBouncer") { keyguardInteractor.primaryBouncerShowing .filterRelevantKeyguardStateAnd { primaryBouncerShowing -> primaryBouncerShowing } - .collect { startTransitionTo(KeyguardState.PRIMARY_BOUNCER) } + .collect { + // Bouncer shows on top of the hub, so do not change scenes here. + startTransitionTo(KeyguardState.PRIMARY_BOUNCER) + } } } @@ -130,7 +141,10 @@ constructor( .filterRelevantKeyguardStateAnd { alternateBouncerShowing -> alternateBouncerShowing } - .collect { pair -> startTransitionTo(KeyguardState.ALTERNATE_BOUNCER) } + .collect { pair -> + // Bouncer shows on top of the hub, so do not change scenes here. + startTransitionTo(KeyguardState.ALTERNATE_BOUNCER) + } } } @@ -139,10 +153,18 @@ constructor( powerInteractor.isAsleep .filterRelevantKeyguardStateAnd { isAsleep -> isAsleep } .collect { - startTransitionTo( - toState = KeyguardState.DOZING, - modeOnCanceled = TransitionModeOnCanceled.LAST_VALUE, - ) + if (communalSceneKtfRefactor()) { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + transitionKey = CommunalTransitionKeys.Immediately, + keyguardState = KeyguardState.DOZING, + ) + } else { + startTransitionTo( + toState = KeyguardState.DOZING, + modeOnCanceled = TransitionModeOnCanceled.LAST_VALUE, + ) + } } } } @@ -152,7 +174,44 @@ constructor( scope.launch { keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop .filterRelevantKeyguardStateAnd { onTop -> onTop } - .collect { maybeStartTransitionToOccludedOrInsecureCamera() } + .collect { + maybeStartTransitionToOccludedOrInsecureCamera { state, reason -> + if (communalSceneKtfRefactor()) { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + transitionKey = CommunalTransitionKeys.SimpleFade, + keyguardState = state, + ) + null + } else { + startTransitionTo(state, ownerReason = reason) + } + } + } + } + } else if (communalSceneKtfRefactor()) { + scope.launch { + allOf( + keyguardInteractor.isKeyguardOccluded, + noneOf( + // Dream is a special-case of occluded, so filter out the dreaming + // case here. + keyguardInteractor.isDreaming, + // When launching activities from widgets on the hub, we have a + // custom occlusion animation. + communalSceneInteractor.isLaunchingWidget, + ), + ) + .filterRelevantKeyguardStateAnd { isOccludedAndNotDreamingNorLaunchingWidget -> + isOccludedAndNotDreamingNorLaunchingWidget + } + .collect { _ -> + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + transitionKey = CommunalTransitionKeys.SimpleFade, + keyguardState = KeyguardState.OCCLUDED, + ) + } } } else { scope.launch { @@ -160,9 +219,7 @@ constructor( .filterRelevantKeyguardStateAnd { isOccludedAndNotDreaming -> isOccludedAndNotDreaming } - .collect { isOccludedAndNotDreaming -> - startTransitionTo(KeyguardState.OCCLUDED) - } + .collect { _ -> startTransitionTo(KeyguardState.OCCLUDED) } } } } @@ -170,10 +227,33 @@ constructor( private fun listenForHubToGone() { // TODO(b/336576536): Check if adaptation for scene framework is needed if (SceneContainerFlag.isEnabled) return - scope.launch { - keyguardInteractor.isKeyguardGoingAway - .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway } - .collect { startTransitionTo(KeyguardState.GONE) } + if (communalSceneKtfRefactor()) { + scope.launch { + allOf( + keyguardInteractor.isKeyguardGoingAway, + // TODO(b/327225415): Handle edit mode opening here to avoid going to GONE + // state until after edit mode is ready to be shown. + noneOf( + // When launching activities from widgets on the hub, we wait to change + // scenes until the activity launch is complete. + communalSceneInteractor.isLaunchingWidget, + ), + ) + .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway } + .collect { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + transitionKey = CommunalTransitionKeys.SimpleFade, + keyguardState = KeyguardState.GONE + ) + } + } + } else { + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway } + .collect { startTransitionTo(KeyguardState.GONE) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 5c7adf0c5a8a..16c014f451f3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -20,6 +20,7 @@ import android.animation.ValueAnimator import android.util.MathUtils import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch +import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -90,7 +91,9 @@ constructor( listenForLockscreenToPrimaryBouncerDragging() listenForLockscreenToAlternateBouncer() listenForLockscreenTransitionToCamera() - listenForLockscreenToGlanceableHub() + if (!communalSceneKtfRefactor()) { + listenForLockscreenToGlanceableHub() + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index f3ca9df6491c..2f320409f231 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -18,8 +18,12 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators +import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.Flags.restartDreamOnUnocclude import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -49,6 +53,7 @@ constructor( keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( @@ -140,7 +145,14 @@ constructor( } else if (isIdleOnCommunal || showCommunalFromOccluded) { // TODO(b/336576536): Check if adaptation for scene framework is needed if (SceneContainerFlag.isEnabled) return - startTransitionTo(KeyguardState.GLANCEABLE_HUB) + if (communalSceneKtfRefactor()) { + communalSceneInteractor.changeScene( + CommunalScenes.Communal, + CommunalTransitionKeys.SimpleFade + ) + } else { + startTransitionTo(KeyguardState.GLANCEABLE_HUB) + } } else { startTransitionTo(KeyguardState.LOCKSCREEN) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 24290881a0fb..6c89ce055470 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -113,10 +113,9 @@ constructor( (isBouncerShowing, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal) -> if ( - !maybeStartTransitionToOccludedOrInsecureCamera() && - !isBouncerShowing && - isAwake && - !isActiveDreamLockscreenHosted + !maybeStartTransitionToOccludedOrInsecureCamera { state, reason -> + startTransitionTo(state, ownerReason = reason) + } && !isBouncerShowing && isAwake && !isActiveDreamLockscreenHosted ) { val toState = if (isIdleOnCommunal) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index ec03a6d8121f..046e79cd04c4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -188,7 +188,7 @@ constructor( * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true, * but not vice-versa. */ - val isDreaming: Flow<Boolean> = repository.isDreaming + val isDreaming: StateFlow<Boolean> = repository.isDreaming /** Whether the system is dreaming with an overlay active */ val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay @@ -205,7 +205,8 @@ constructor( trySendWithFailureLogging( cameraLaunchSourceIntToModel(source), TAG, - "updated onCameraLaunchGestureDetected") + "updated onCameraLaunchGestureDetected" + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index 7a06d2fe9254..cd49c6a4d2e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt @@ -129,13 +129,16 @@ constructor( } } - /** Notifies that the user has long-pressed on the lock screen. */ - fun onLongPress() { + /** Notifies that the user has long-pressed on the lock screen. + * + * @param isA11yAction: Whether the action was performed as an a11y action + */ + fun onLongPress(isA11yAction: Boolean = false) { if (!isLongPressHandlingEnabled.value) { return } - if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) { + if (isA11yAction) { showSettings() } else { showMenu() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index 89c717892d0a..d06ee645652c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -122,9 +122,14 @@ sealed class TransitionInteractor( * SHOW_WHEN_LOCKED activity, or back to [KeyguardState.GONE], for some power button launch * gesture cases. If so, start the transition. * + * @param startTransition A callback which is triggered to start the transition to the desired + * KeyguardState. Allows caller to hook into the transition start if needed. + * * Returns true if a transition was started, false otherwise. */ - suspend fun maybeStartTransitionToOccludedOrInsecureCamera(): Boolean { + suspend fun maybeStartTransitionToOccludedOrInsecureCamera( + startTransition: suspend (state: KeyguardState, reason: String) -> UUID? + ): Boolean { // The refactor is required for the occlusion interactor to work. KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode() @@ -136,10 +141,7 @@ sealed class TransitionInteractor( if (!maybeHandleInsecurePowerGesture()) { // Otherwise, the double tap gesture occurred while not GONE and not dismissable, // which means we will launch the secure camera, which OCCLUDES the keyguard. - startTransitionTo( - KeyguardState.OCCLUDED, - ownerReason = "Power button gesture on lockscreen" - ) + startTransition(KeyguardState.OCCLUDED, "Power button gesture on lockscreen") } return true @@ -147,10 +149,7 @@ sealed class TransitionInteractor( // A SHOW_WHEN_LOCKED activity is on top of the task stack. Transition to OCCLUDED so // it's visible. // TODO(b/307976454) - Centralize transition to DREAMING here. - startTransitionTo( - KeyguardState.OCCLUDED, - ownerReason = "SHOW_WHEN_LOCKED activity on top" - ) + startTransition(KeyguardState.OCCLUDED, "SHOW_WHEN_LOCKED activity on top") return true } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 76f7749754a2..1b9788f2d401 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -74,8 +74,8 @@ object DeviceEntryIconViewBinder { val bgView = view.bgView longPressHandlingView.listener = object : LongPressHandlingView.Listener { - override fun onLongPressDetected(view: View, x: Int, y: Int) { - if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { + override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) { + if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { return } vibratorHelper.performHapticFeedback( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt index 057b4f9a671d..b387855ad553 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt @@ -18,12 +18,15 @@ package com.android.systemui.keyguard.ui.binder import android.view.View +import android.view.accessibility.AccessibilityNodeInfo +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launch import com.android.systemui.common.ui.view.LongPressHandlingView import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import com.android.systemui.plugins.FalsingManager object KeyguardLongPressViewBinder { @@ -43,14 +46,19 @@ object KeyguardLongPressViewBinder { onSingleTap: () -> Unit, falsingManager: FalsingManager, ) { + view.accessibilityHintLongPressAction = + AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfoCompat.ACTION_LONG_CLICK, + view.resources.getString(R.string.lock_screen_settings) + ) view.listener = object : LongPressHandlingView.Listener { - override fun onLongPressDetected(view: View, x: Int, y: Int) { - if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { + override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) { + if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { return } - viewModel.onLongPress() + viewModel.onLongPress(isA11yAction) } override fun onSingleTapDetected(view: View) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt index 940d1e10651a..0532ee285d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt @@ -25,6 +25,8 @@ import android.os.Messenger import android.util.ArrayMap import android.util.Log import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.runBlocking +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -55,7 +57,14 @@ constructor( var observer: PreviewLifecycleObserver? = null return try { - val renderer = previewRendererFactory.create(request) + val renderer = + if (Flags.lockscreenPreviewRendererCreateOnMainThread()) { + runBlocking ("$TAG#previewRendererFactory.create", mainDispatcher) { + previewRendererFactory.create(request) + } + } else { + previewRendererFactory.create(request) + } observer = PreviewLifecycleObserver( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt index bb4fb7961ed0..e9db1d23ec3c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt @@ -83,6 +83,7 @@ constructor( MathUtils.lerp(startAlpha, 0f, it) } }, + onFinish = { 1f }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt index f1cbf256a00f..1d2edc6d406b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt @@ -33,9 +33,12 @@ constructor( /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled - /** Notifies that the user has long-pressed on the lock screen. */ - fun onLongPress() { - interactor.onLongPress() + /** Notifies that the user has long-pressed on the lock screen. + * + * @param isA11yAction: Whether the action was performed as an a11y action + */ + fun onLongPress(isA11yAction: Boolean) { + interactor.onLongPress(isA11yAction) } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 46ba5d13bb6d..8811908e1d45 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -80,6 +80,7 @@ constructor( 1f - it } }, + onFinish = { 1f }, ) /** Bouncer container alpha */ diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index d1c9b8ea1803..b2ba0e1cd6a6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -169,6 +169,14 @@ public class LogModule { return factory.create("NotifRemoteInputLog", 50 /* maxSize */, false /* systrace */); } + /** Provides a logging buffer for all logs related to notification visual stability. */ + @Provides + @SysUISingleton + @VisualStabilityLog + public static LogBuffer provideVisualStabilityLogBuffer(LogBufferFactory factory) { + return factory.create("VisualStabilityLog", 50 /* maxSize */, false /* systrace */); + } + /** Provides a logging buffer for all logs related to keyguard media controller. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt index 9be7dfe9a1a9..b45ffc1128b1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.stack.data.repository +package com.android.systemui.log.dagger -import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import javax.inject.Qualifier -fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) { - setNotifications(*notifications.toTypedArray()) -} - -fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) { - this.activeHeadsUpRows.value = notifications.toSet() -} +/** A [com.android.systemui.log.LogBuffer] for visual stability-related messages. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class VisualStabilityLog diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index f004c3a8916f..6c5337418d02 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -187,44 +187,17 @@ public class MediaProjectionPermissionActivity extends Activity } } - TextPaint paint = new TextPaint(); - paint.setTextSize(42); - CharSequence dialogText = null; CharSequence dialogTitle = null; - String appName = null; - if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) { + + final String appName = extractAppName(aInfo, packageManager); + final boolean hasCastingCapabilities = + Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName); + + if (hasCastingCapabilities) { dialogText = getString(R.string.media_projection_sys_service_dialog_warning); dialogTitle = getString(R.string.media_projection_sys_service_dialog_title); } else { - String label = aInfo.loadLabel(packageManager).toString(); - - // If the label contains new line characters it may push the security - // message below the fold of the dialog. Labels shouldn't have new line - // characters anyways, so just truncate the message the first time one - // is seen. - final int labelLength = label.length(); - int offset = 0; - while (offset < labelLength) { - final int codePoint = label.codePointAt(offset); - final int type = Character.getType(codePoint); - if (type == Character.LINE_SEPARATOR - || type == Character.CONTROL - || type == Character.PARAGRAPH_SEPARATOR) { - label = label.substring(0, offset) + ELLIPSIS; - break; - } - offset += Character.charCount(codePoint); - } - - if (label.isEmpty()) { - label = mPackageName; - } - - String unsanitizedAppName = TextUtils.ellipsize(label, - paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString(); - appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName); - String actionText = getString(R.string.media_projection_dialog_warning, appName); SpannableString message = new SpannableString(actionText); @@ -255,6 +228,7 @@ public class MediaProjectionPermissionActivity extends Activity grantMediaProjectionPermission(selectedOption.getMode()); }, () -> finish(RECORD_CANCEL, /* projection= */ null), + hasCastingCapabilities, appName, overrideDisableSingleAppOption, mUid, @@ -289,6 +263,47 @@ public class MediaProjectionPermissionActivity extends Activity } } + private String extractAppName(ApplicationInfo applicationInfo, PackageManager packageManager) { + String label = applicationInfo.loadLabel(packageManager).toString(); + + // If the label contains new line characters it may push the security + // message below the fold of the dialog. Labels shouldn't have new line + // characters anyways, so just truncate the message the first time one + // is seen. + final int labelLength = label.length(); + int offset = 0; + while (offset < labelLength) { + final int codePoint = label.codePointAt(offset); + final int type = Character.getType(codePoint); + if (type == Character.LINE_SEPARATOR + || type == Character.CONTROL + || type == Character.PARAGRAPH_SEPARATOR) { + label = label.substring(0, offset) + ELLIPSIS; + break; + } + offset += Character.charCount(codePoint); + } + + if (label.isEmpty()) { + label = mPackageName; + } + + TextPaint paint = new TextPaint(); + paint.setTextSize(42); + + String unsanitizedAppName = TextUtils.ellipsize(label, + paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString(); + String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName); + + // Have app name be the package name as a default fallback, if specific app name can't be + // extracted + if (appName == null || appName.isEmpty()) { + return mPackageName; + } + + return appName; + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt index 9ce8070131fa..6d1a4587d089 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt @@ -29,13 +29,20 @@ class MediaProjectionPermissionDialogDelegate( mediaProjectionConfig: MediaProjectionConfig?, private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>, private val onCancelClicked: Runnable, - private val appName: String?, + private val hasCastingCapabilities: Boolean, + appName: String, forceShowPartialScreenshare: Boolean, hostUid: Int, mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, ) : BaseMediaProjectionPermissionDialogDelegate<AlertDialog>( - createOptionList(context, appName, mediaProjectionConfig, forceShowPartialScreenshare), + createOptionList( + context, + appName, + hasCastingCapabilities, + mediaProjectionConfig, + forceShowPartialScreenshare + ), appName, hostUid, mediaProjectionMetricsLogger @@ -43,7 +50,7 @@ class MediaProjectionPermissionDialogDelegate( override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) { super.onCreate(dialog, savedInstanceState) // TODO(b/270018943): Handle the case of System sharing (not recording nor casting) - if (appName == null) { + if (hasCastingCapabilities) { setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title) setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue) } else { @@ -65,30 +72,29 @@ class MediaProjectionPermissionDialogDelegate( companion object { private fun createOptionList( context: Context, - appName: String?, + appName: String, + hasCastingCapabilities: Boolean, mediaProjectionConfig: MediaProjectionConfig?, overrideDisableSingleAppOption: Boolean = false, ): List<ScreenShareOption> { val singleAppWarningText = - if (appName == null) { + if (hasCastingCapabilities) { R.string.media_projection_entry_cast_permission_dialog_warning_single_app } else { R.string.media_projection_entry_app_permission_dialog_warning_single_app } val entireScreenWarningText = - if (appName == null) { + if (hasCastingCapabilities) { R.string.media_projection_entry_cast_permission_dialog_warning_entire_screen } else { R.string.media_projection_entry_app_permission_dialog_warning_entire_screen } - // The single app option should only be disabled if there is an app name provided, - // the client has setup a MediaProjection with - // MediaProjectionConfig#createConfigForDefaultDisplay, AND it hasn't been overridden by - // the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override. + // The single app option should only be disabled if the client has setup a + // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND + // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override. val singleAppOptionDisabled = - appName != null && - !overrideDisableSingleAppOption && + !overrideDisableSingleAppOption && mediaProjectionConfig?.regionToCapture == MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index 720120b630d5..5ea8c2183295 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -14,6 +14,8 @@ package com.android.systemui.qs.tileimpl; +import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ArgbEvaluator; @@ -204,6 +206,9 @@ public class QSIconViewImpl extends QSIconView { values.setEvaluator(ArgbEvaluator.getInstance()); mColorAnimator.setValues(values); mColorAnimator.removeAllListeners(); + if (removeUpdateListenerInQsIconViewImpl()) { + mColorAnimator.removeAllUpdateListeners(); + } mColorAnimator.addUpdateListener(animation -> { setTint(iv, (int) animation.getAnimatedValue()); }); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 6b3dfe1b90ad..dbfe8188b1b5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -107,7 +107,9 @@ constructor( set(value) { if (field == value) return field = value - updateHeight() + if (longPressEffect?.state != QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) { + updateHeight() + } } override var squishinessFraction: Float = 1f @@ -381,14 +383,6 @@ constructor( } private fun updateHeight() { - // TODO(b/332900989): Find a more robust way of resetting the tile if not reset by the - // launch animation. - if (!haveLongPressPropertiesBeenReset && longPressEffect != null) { - // The launch animation of a long-press effect did not reset the long-press effect so - // we must do it here - resetLongPressEffectProperties() - longPressEffect.resetState() - } val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) { heightOverride @@ -417,17 +411,17 @@ constructor( } override fun init(tile: QSTile) { - val expandable = Expandable.fromView(this) if (longPressEffect != null) { isHapticFeedbackEnabled = false longPressEffect.qsTile = tile - longPressEffect.expandable = expandable + longPressEffect.createExpandableFromView(this) initLongPressEffectCallback() init( { _: View -> longPressEffect.onTileClick() }, null, // Haptics and long-clicks will be handled by the [QSLongPressEffect] ) } else { + val expandable = Expandable.fromView(this) init( { _: View? -> tile.click(expandable) }, { _: View? -> @@ -475,10 +469,10 @@ constructor( } } - override fun onReverseAnimator() { + override fun onReverseAnimator(playHaptics: Boolean) { longPressEffectAnimator?.let { val pausedProgress = it.animatedFraction - longPressEffect?.playReverseHaptics(pausedProgress) + if (playHaptics) longPressEffect?.playReverseHaptics(pausedProgress) it.reverse() } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 51447cc6f373..72f37fc98e17 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -187,7 +187,6 @@ constructor( applicationScope.launch { // TODO(b/296114544): Combine with some global hun state to make it visible! deviceProvisioningInteractor.isDeviceProvisioned - .distinctUntilChanged() .flatMapLatest { isAllowedToBeVisible -> if (isAllowedToBeVisible) { combine( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 49144091cb62..54ae225442c9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Random; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; /** * An AsyncTask that saves an image to the media store in the background. @@ -59,12 +60,73 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s"; private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; + /** + * POD used in the AsyncTask which saves an image in the background. + */ + static class SaveImageInBackgroundData { + public Bitmap image; + public Consumer<Uri> finisher; + public ActionsReadyListener mActionsReadyListener; + public QuickShareActionReadyListener mQuickShareActionsReadyListener; + public UserHandle owner; + public int displayId; + + void clearImage() { + image = null; + } + } + + /** + * Structure returned by the SaveImageInBackgroundTask + */ + public static class SavedImageData { + public Uri uri; + public List<Notification.Action> smartActions; + public Notification.Action quickShareAction; + public UserHandle owner; + public String subject; // Title for sharing + public Long imageTime; // Time at which screenshot was saved + + /** + * Used to reset the return data on error + */ + public void reset() { + uri = null; + smartActions = null; + quickShareAction = null; + subject = null; + imageTime = null; + } + } + + /** + * Structure returned by the QueryQuickShareInBackgroundTask + */ + static class QuickShareData { + public Notification.Action quickShareAction; + + /** + * Used to reset the return data on error + */ + public void reset() { + quickShareAction = null; + } + } + + interface ActionsReadyListener { + void onActionsReady(SavedImageData imageData); + } + + interface QuickShareActionReadyListener { + void onActionsReady(QuickShareData quickShareData); + } + private final Context mContext; private FeatureFlags mFlags; private final ScreenshotSmartActions mScreenshotSmartActions; - private final ScreenshotController.SaveImageInBackgroundData mParams; - private final ScreenshotController.SavedImageData mImageData; - private final ScreenshotController.QuickShareData mQuickShareData; + private final SaveImageInBackgroundData mParams; + private final SavedImageData mImageData; + private final QuickShareData mQuickShareData; private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; private String mScreenshotId; @@ -77,15 +139,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { FeatureFlags flags, ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, - ScreenshotController.SaveImageInBackgroundData data, + SaveImageInBackgroundData data, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider ) { mContext = context; mFlags = flags; mScreenshotSmartActions = screenshotSmartActions; - mImageData = new ScreenshotController.SavedImageData(); - mQuickShareData = new ScreenshotController.QuickShareData(); + mImageData = new SavedImageData(); + mQuickShareData = new QuickShareData(); mImageExporter = exporter; // Prepare all the output metadata @@ -195,7 +257,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { * Update the listener run when the saving task completes. Used to avoid showing UI for the * first screenshot when a second one is taken. */ - void setActionsReadyListener(ScreenshotController.ActionsReadyListener listener) { + void setActionsReadyListener(ActionsReadyListener listener) { mParams.mActionsReadyListener = listener; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 773900981759..0a4635e0849b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 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. @@ -35,10 +35,7 @@ import android.animation.AnimatorListenerAdapter; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityOptions; -import android.app.ExitTransitionCoordinator; import android.app.ICompatCameraControlCallback; -import android.app.Notification; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -55,7 +52,6 @@ import android.os.UserManager; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; -import android.util.Pair; import android.view.Display; import android.view.ScrollCaptureResponse; import android.view.View; @@ -67,7 +63,6 @@ import android.view.WindowManager; import android.widget.Toast; import android.window.WindowContext; -import com.android.internal.app.ChooserActivity; import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; import com.android.settingslib.applications.InterestingConfigChanges; @@ -89,7 +84,6 @@ import dagger.assisted.AssistedInject; import kotlin.Unit; -import java.util.List; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; @@ -104,67 +98,6 @@ import javax.inject.Provider; public class ScreenshotController implements ScreenshotHandler { private static final String TAG = logTag(ScreenshotController.class); - /** - * POD used in the AsyncTask which saves an image in the background. - */ - static class SaveImageInBackgroundData { - public Bitmap image; - public Consumer<Uri> finisher; - public ScreenshotController.ActionsReadyListener mActionsReadyListener; - public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener; - public UserHandle owner; - public int displayId; - - void clearImage() { - image = null; - } - } - - /** - * Structure returned by the SaveImageInBackgroundTask - */ - public static class SavedImageData { - public Uri uri; - public List<Notification.Action> smartActions; - public Notification.Action quickShareAction; - public UserHandle owner; - public String subject; // Title for sharing - public Long imageTime; // Time at which screenshot was saved - - /** - * Used to reset the return data on error - */ - public void reset() { - uri = null; - smartActions = null; - quickShareAction = null; - subject = null; - imageTime = null; - } - } - - /** - * Structure returned by the QueryQuickShareInBackgroundTask - */ - static class QuickShareData { - public Notification.Action quickShareAction; - - /** - * Used to reset the return data on error - */ - public void reset() { - quickShareAction = null; - } - } - - interface ActionsReadyListener { - void onActionsReady(ScreenshotController.SavedImageData imageData); - } - - interface QuickShareActionReadyListener { - void onActionsReady(ScreenshotController.QuickShareData quickShareData); - } - public interface TransitionDestination { /** * Allows the long screenshot activity to call back with a destination location (the bounds @@ -213,7 +146,6 @@ public class ScreenshotController implements ScreenshotHandler { private final ScreenshotNotificationSmartActionsProvider mScreenshotNotificationSmartActionsProvider; private final TimeoutHandler mScreenshotHandler; - private final ActionIntentExecutor mActionIntentExecutor; private final UserManager mUserManager; private final AssistContentRequester mAssistContentRequester; private final ActionExecutor mActionExecutor; @@ -259,7 +191,6 @@ public class ScreenshotController implements ScreenshotHandler { BroadcastDispatcher broadcastDispatcher, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ScreenshotActionsController.Factory screenshotActionsControllerFactory, - ActionIntentExecutor actionIntentExecutor, ActionExecutor.Factory actionExecutorFactory, UserManager userManager, AssistContentRequester assistContentRequester, @@ -289,7 +220,6 @@ public class ScreenshotController implements ScreenshotHandler { final Context displayContext = context.createDisplayContext(display); mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); mFlags = flags; - mActionIntentExecutor = actionIntentExecutor; mUserManager = userManager; mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; @@ -765,33 +695,6 @@ public class ScreenshotController implements ScreenshotHandler { mScreenshotAnimation.start(); } - /** - * Supplies the necessary bits for the shared element transition to share sheet. - * Note that once called, the action intent to share must be sent immediately after. - */ - private Pair<ActivityOptions, ExitTransitionCoordinator> createWindowTransition() { - ExitTransitionCoordinator.ExitTransitionCallbacks callbacks = - new ExitTransitionCoordinator.ExitTransitionCallbacks() { - @Override - public boolean isReturnTransitionAllowed() { - return false; - } - - @Override - public void hideSharedElements() { - finishDismiss(); - } - - @Override - public void onFinish() { - } - }; - - return ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null, - Pair.create(mViewProxy.getScreenshotPreview(), - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); - } - /** Reset screenshot view and then call onCompleteRunnable */ private void finishDismiss() { Log.d(TAG, "finishDismiss"); @@ -838,11 +741,11 @@ public class ScreenshotController implements ScreenshotHandler { private void saveScreenshotInWorkerThread( UserHandle owner, @NonNull Consumer<Uri> finisher, - @Nullable ActionsReadyListener actionsReadyListener, - @Nullable QuickShareActionReadyListener + @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener, + @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener quickShareActionsReadyListener) { - ScreenshotController.SaveImageInBackgroundData - data = new ScreenshotController.SaveImageInBackgroundData(); + SaveImageInBackgroundTask.SaveImageInBackgroundData + data = new SaveImageInBackgroundTask.SaveImageInBackgroundData(); data.image = mScreenBitmap; data.finisher = finisher; data.mActionsReadyListener = actionsReadyListener; @@ -881,7 +784,7 @@ public class ScreenshotController implements ScreenshotHandler { /** * Logs success/failure of the screenshot saving task, and shows an error if it failed. */ - private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) { + private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) { logScreenshotResultStatus(imageData.uri, imageData.owner); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt index 4f27b9e4dbf0..b3d5c9e9691c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt @@ -99,7 +99,7 @@ class PolicyRequestProcessor( original, updates.component, updates.owner, - type.displayId + type.displayId, ) } return updated @@ -120,6 +120,7 @@ class PolicyRequestProcessor( return replaceWithScreenshot( original = original, componentName = topMainRootTask?.topActivity ?: defaultComponent, + taskId = topMainRootTask?.taskId, owner = defaultOwner, displayId = original.displayId ) @@ -144,11 +145,12 @@ class PolicyRequestProcessor( ) } - suspend fun replaceWithScreenshot( + private suspend fun replaceWithScreenshot( original: ScreenshotData, componentName: ComponentName?, owner: UserHandle?, displayId: Int, + taskId: Int? = null, ): ScreenshotData { Log.i(TAG, "Capturing screenshot: $componentName / $owner") val screenshot = captureDisplay(displayId) @@ -157,7 +159,8 @@ class PolicyRequestProcessor( bitmap = screenshot, userHandle = owner, topComponent = componentName, - screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0) + screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0), + taskId = taskId ?: -1, ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index d870fe69463d..b468d0e75a7a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -50,19 +50,21 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf -import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.collectFlow import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** @@ -77,6 +79,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val communalViewModel: CommunalViewModel, private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, private val powerManager: PowerManager, private val communalColors: CommunalColors, @@ -149,6 +152,19 @@ constructor( private var hubShowing = false /** + * True if we're transitioning to or from edit mode + * + * We block all touches and gestures when edit mode is open to prevent funky transition issues + * when entering and exiting edit mode because we delay exiting the hub scene when entering edit + * mode and enter the hub scene early when exiting edit mode to make for a smoother transition. + * Gestures during these transitions can result in broken and unexpected UI states. + * + * Tracks [CommunalInteractor.editActivityShowing] and the [KeyguardState.GONE] to + * [KeyguardState.GLANCEABLE_HUB] transition. + */ + private var inEditModeTransition = false + + /** * True if either the primary or alternate bouncer are open, meaning the hub should not receive * any touch input. */ @@ -165,7 +181,14 @@ constructor( * * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting]. */ - private var shadeShowing = false + private var shadeShowingAndConsumingTouches = false + + /** + * True if the shade ever fully expands and the user isn't interacting with it (aka finger on + * screen dragging). In this case, the shade should handle all touch events until it has fully + * collapsed. + */ + private var userNotInteractiveAtShadeFullyExpanded = false /** * True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes @@ -317,9 +340,41 @@ constructor( ) collectFlow( containerView, - allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)), + // When leaving edit mode, editActivityShowing is true until the edit mode activity + // finishes itself and the device locks, after which isInTransition will be true until + // we're fully on the hub. + anyOf( + communalInteractor.editActivityShowing, + keyguardTransitionInteractor.isInTransition( + Edge.create(KeyguardState.GONE, KeyguardState.GLANCEABLE_HUB) + ) + ), { - shadeShowing = it + inEditModeTransition = it + updateTouchHandlingState() + } + ) + collectFlow( + containerView, + combine( + shadeInteractor.isAnyFullyExpanded, + shadeInteractor.isUserInteracting, + shadeInteractor.isShadeFullyCollapsed, + ::Triple + ), + { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) -> + val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting + + // If we ever are fully expanded and not interacting, capture this state as we + // should not handle touches until we fully collapse again + userNotInteractiveAtShadeFullyExpanded = + !isShadeFullyCollapsed && + (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive) + + // If the shade reaches full expansion without interaction, then we should allow it + // to consume touches rather than handling it here until it disappears. + shadeShowingAndConsumingTouches = + userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive updateTouchHandlingState() } ) @@ -337,7 +392,11 @@ constructor( * Also clears gesture exclusion zones when the hub is occluded or gone. */ private fun updateTouchHandlingState() { - val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing) + // Only listen to gestures when we're settled in the hub keyguard state and the shade + // bouncer are not showing on top. + val shouldInterceptGestures = + hubShowing && + !(shadeShowingAndConsumingTouches || anyBouncerShowing || inEditModeTransition) if (shouldInterceptGestures) { lifecycleRegistry.currentState = Lifecycle.State.RESUMED } else { @@ -389,17 +448,18 @@ constructor( return false } - return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false + return communalContainerView?.let { handleTouchEventOnCommunalView(ev) } ?: false } - private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean { + private fun handleTouchEventOnCommunalView(ev: MotionEvent): Boolean { val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN val isUp = ev.actionMasked == MotionEvent.ACTION_UP + val isMove = ev.actionMasked == MotionEvent.ACTION_MOVE val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL - val hubOccluded = anyBouncerShowing || shadeShowing + val hubOccluded = anyBouncerShowing || shadeShowingAndConsumingTouches - if (isDown && !hubOccluded) { + if ((isDown || isMove) && !hubOccluded) { isTrackingHubTouch = true } @@ -407,7 +467,7 @@ constructor( if (isUp || isCancel) { isTrackingHubTouch = false } - return dispatchTouchEvent(view, ev) + return dispatchTouchEvent(ev) } return false @@ -417,7 +477,14 @@ constructor( * Dispatches the touch event to the communal container and sends a user activity event to reset * the screen timeout. */ - private fun dispatchTouchEvent(view: View, ev: MotionEvent): Boolean { + private fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (inEditModeTransition) { + // Consume but ignore touches while we're transitioning to or from edit mode so that the + // user can't trigger another transition, such as by swiping the hub away, tapping a + // widget, or opening the shade/bouncer. Doing any of these while transitioning can + // result in broken states. + return true + } try { var handled = false communalContainerWrapper?.dispatchTouchEvent(ev) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 5065baa04623..23e2620ac6d6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -76,7 +76,7 @@ constructor( scope.launch { shadeInteractor.isAnyExpanded.collect { if (!it) { - runPostCollapseActions() + withContext(mainDispatcher) { runPostCollapseActions() } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index c9126161c40f..b9d24abc4037 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -51,6 +51,7 @@ import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; import android.util.Pair; import android.util.SparseArray; import android.view.KeyEvent; @@ -200,6 +201,7 @@ public class CommandQueue extends IStatusBar.Stub implements * event. */ private int mLastUpdatedImeDisplayId = INVALID_DISPLAY; + private final Context mContext; private final DisplayTracker mDisplayTracker; private final @Nullable CommandRegistry mRegistry; private final @Nullable DumpHandler mDumpHandler; @@ -571,6 +573,7 @@ public class CommandQueue extends IStatusBar.Stub implements DumpHandler dumpHandler, Lazy<PowerInteractor> powerInteractor ) { + mContext = context; mDisplayTracker = displayTracker; mRegistry = registry; mDumpHandler = dumpHandler; @@ -1209,7 +1212,12 @@ public class CommandQueue extends IStatusBar.Stub implements boolean showImeSwitcher) { if (displayId == INVALID_DISPLAY) return; - if (mLastUpdatedImeDisplayId != displayId + boolean isConcurrentMultiUserModeEnabled = UserManager.isVisibleBackgroundUsersEnabled() + && mContext.getResources().getBoolean(android.R.bool.config_perDisplayFocusEnabled) + && android.view.inputmethod.Flags.concurrentInputMethods(); + + if (!isConcurrentMultiUserModeEnabled + && mLastUpdatedImeDisplayId != displayId && mLastUpdatedImeDisplayId != INVALID_DISPLAY) { // Set previous NavBar's IME window status as invisible when IME // window switched to another display for single-session IME case. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index b2140f793ced..130b1170c9f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.ui.model import android.content.Context import android.content.res.ColorStateList +import android.view.ContextThemeWrapper import androidx.annotation.ColorInt import com.android.settingslib.Utils import com.android.systemui.res.R @@ -41,8 +42,11 @@ sealed interface ColorsModel { /** The chip should have a red background with white text. */ data object Red : ColorsModel { - override fun background(context: Context): ColorStateList = - ColorStateList.valueOf(context.getColor(R.color.GM2_red_600)) + override fun background(context: Context): ColorStateList { + val themedContext = + ContextThemeWrapper(context, com.android.internal.R.style.Theme_DeviceDefault_Light) + return Utils.getColorAttr(themedContext, com.android.internal.R.attr.materialColorError) + } override fun text(context: Context) = context.getColor(android.R.color.white) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt index 3dcaff3de35a..b342722ebb09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt @@ -52,8 +52,11 @@ constructor(val proxy: DeviceConfigProxy, val context: Context) { } fun getNotificationBuckets(): IntArray { - if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled - || NotificationClassificationFlag.isEnabled) { + if ( + PriorityPeopleSection.isEnabled || + NotificationMinimalismPrototype.isEnabled || + NotificationClassificationFlag.isEnabled + ) { // We don't need this list to be adaptive, it can be the superset of all features. return PriorityBucket.getAllInOrder() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index e48c28d3f3ee..cb133ecadab2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -1005,6 +1005,16 @@ public final class NotificationEntry extends ListEntry implements NotificationRo mIsMarkedForUserTriggeredMovement = marked; } + private boolean mSeenInShade = false; + + public void setSeenInShade(boolean seen) { + mSeenInShade = seen; + } + + public boolean isSeenInShade() { + return mSeenInShade; + } + public void setIsHeadsUpEntry(boolean isHeadsUpEntry) { mIsHeadsUpEntry = isHeadsUpEntry; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt new file mode 100644 index 000000000000..a6605f652ff3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt @@ -0,0 +1,295 @@ +/* + * 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.systemui.statusbar.notification.collection.coordinator + +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.os.UserHandle +import android.provider.Settings +import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING +import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN +import com.android.systemui.util.asIndenting +import com.android.systemui.util.printCollection +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.io.PrintWriter +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +/** + * If the setting is enabled, this will track seen notifications and ensure that they only show in + * the shelf on the lockscreen. + * + * This class is a replacement of the [OriginalUnseenKeyguardCoordinator]. + */ +@CoordinatorScope +@SuppressLint("SharedFlowCreation") +class LockScreenMinimalismCoordinator +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val dumpManager: DumpManager, + private val headsUpInteractor: HeadsUpNotificationInteractor, + private val logger: LockScreenMinimalismCoordinatorLogger, + @Application private val scope: CoroutineScope, + private val secureSettings: SecureSettings, + private val seenNotificationsInteractor: SeenNotificationsInteractor, + private val statusBarStateController: StatusBarStateController, + private val shadeInteractor: ShadeInteractor, +) : Coordinator, Dumpable { + + private val unseenNotifications = mutableSetOf<NotificationEntry>() + private var isShadeVisible = false + private var unseenFilterEnabled = false + + override fun attach(pipeline: NotifPipeline) { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) { + return + } + pipeline.addPromoter(unseenNotifPromoter) + pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) + pipeline.addCollectionListener(collectionListener) + scope.launch { trackUnseenFilterSettingChanges() } + dumpManager.registerDumpable(this) + } + + private suspend fun trackSeenNotifications() { + coroutineScope { + launch { clearUnseenNotificationsWhenShadeIsExpanded() } + launch { markHeadsUpNotificationsAsSeen() } + } + } + + private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { + shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded -> + // Give keyguard events time to propagate, in case this expansion is part of the + // keyguard transition and not the user expanding the shade + delay(SHADE_VISIBLE_SEEN_TIMEOUT) + isShadeVisible = isExpanded + if (isExpanded) { + logger.logShadeVisible(unseenNotifications.size) + unseenNotifications.clear() + // no need to invalidateList; filtering is inactive while shade is open + } else { + logger.logShadeHidden() + } + } + } + + private suspend fun markHeadsUpNotificationsAsSeen() { + headsUpInteractor.topHeadsUpRowIfPinned + .map { it?.let { headsUpInteractor.notificationKey(it) } } + .collectLatest { key -> + if (key == null) { + logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false) + } else { + val wasUnseenWhenPinned = unseenNotifications.any { it.key == key } + logger.logTopHeadsUpRow(key, wasUnseenWhenPinned) + if (wasUnseenWhenPinned) { + delay(HEADS_UP_SEEN_TIMEOUT) + val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key } + logger.logHunHasBeenSeen(key, wasUnseenAfterDelay) + // no need to invalidateList; nothing should change until after heads up + } + } + } + } + + private fun unseenFeatureEnabled(): Flow<Boolean> { + // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here? + // Or should we actually just repurpose using the existing setting? + if (NotificationMinimalismPrototype.isEnabled) { + return flowOf(true) + } + return secureSettings + // emit whenever the setting has changed + .observerFlow( + UserHandle.USER_ALL, + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getIntForUser( + name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + def = 0, + userHandle = UserHandle.USER_CURRENT, + ) == 1 + } + // don't emit anything if nothing has changed + .distinctUntilChanged() + // perform lookups on the bg thread pool + .flowOn(bgDispatcher) + // only track the most recent emission, if events are happening faster than they can be + // consumed + .conflate() + } + + private suspend fun trackUnseenFilterSettingChanges() { + unseenFeatureEnabled().collectLatest { isSettingEnabled -> + // update local field and invalidate if necessary + if (isSettingEnabled != unseenFilterEnabled) { + unseenFilterEnabled = isSettingEnabled + unseenNotifPromoter.invalidateList("unseen setting changed") + } + // if the setting is enabled, then start tracking and filtering unseen notifications + logger.logTrackingUnseen(isSettingEnabled) + if (isSettingEnabled) { + trackSeenNotifications() + } + } + } + + private val collectionListener = + object : NotifCollectionListener { + override fun onEntryAdded(entry: NotificationEntry) { + if (!isShadeVisible) { + logger.logUnseenAdded(entry.key) + unseenNotifications.add(entry) + } + } + + override fun onEntryUpdated(entry: NotificationEntry) { + if (!isShadeVisible) { + logger.logUnseenUpdated(entry.key) + unseenNotifications.add(entry) + } + } + + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + if (unseenNotifications.remove(entry)) { + logger.logUnseenRemoved(entry.key) + } + } + } + + private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + // Only ever elevate a top unseen notification on keyguard, not even locked shade + if (statusBarStateController.state != StatusBarState.KEYGUARD) { + seenNotificationsInteractor.setTopOngoingNotification(null) + seenNotificationsInteractor.setTopUnseenNotification(null) + return + } + // On keyguard pick the top-ranked unseen or ongoing notification to elevate + val nonSummaryEntries: Sequence<NotificationEntry> = + list + .asSequence() + .flatMap { + when (it) { + is NotificationEntry -> listOfNotNull(it) + is GroupEntry -> it.children + else -> error("unhandled type of $it") + } + } + .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT } + seenNotificationsInteractor.setTopOngoingNotification( + nonSummaryEntries + .filter { ColorizedFgsCoordinator.isRichOngoing(it) } + .minByOrNull { it.ranking.rank } + ) + seenNotificationsInteractor.setTopUnseenNotification( + nonSummaryEntries + .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications } + .minByOrNull { it.ranking.rank } + ) + } + + @VisibleForTesting + val unseenNotifPromoter = + object : NotifPromoter(TAG) { + override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = + when { + NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false + seenNotificationsInteractor.isTopOngoingNotification(child) -> true + !NotificationMinimalismPrototype.ungroupTopUnseen -> false + else -> seenNotificationsInteractor.isTopUnseenNotification(child) + } + } + + val topOngoingSectioner = + object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { + override fun isInSection(entry: ListEntry): Boolean { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + return entry.anyEntry { notificationEntry -> + seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) + } + } + } + + val topUnseenSectioner = + object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { + override fun isInSection(entry: ListEntry): Boolean { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + return entry.anyEntry { notificationEntry -> + seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) + } + } + } + + private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) = + when { + predicate(representativeEntry) -> true + this !is GroupEntry -> false + else -> children.any(predicate) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) = + with(pw.asIndenting()) { + seenNotificationsInteractor.dump(this) + printCollection("unseen notifications", unseenNotifications) { println(it.key) } + } + + companion object { + private const val TAG = "LockScreenMinimalismCoordinator" + private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds + private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt new file mode 100644 index 000000000000..e44a77c30999 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt @@ -0,0 +1,99 @@ +/* + * 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.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.UnseenNotificationLog +import javax.inject.Inject + +private const val TAG = "LockScreenMinimalismCoordinator" + +class LockScreenMinimalismCoordinatorLogger +@Inject +constructor( + @UnseenNotificationLog private val buffer: LogBuffer, +) { + + fun logTrackingUnseen(trackingUnseen: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { bool1 = trackingUnseen }, + messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, + ) + + fun logShadeVisible(numUnseen: Int) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { int1 = numUnseen }, + messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" } + ) + } + + fun logShadeHidden() { + buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.") + } + + fun logUnseenAdded(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = key }, + messagePrinter = { "Unseen notif added: $str1" }, + ) + + fun logUnseenUpdated(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = key }, + messagePrinter = { "Unseen notif updated: $str1" }, + ) + + fun logUnseenRemoved(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = key }, + messagePrinter = { "Unseen notif removed: $str1" }, + ) + + fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { + str1 = key + bool1 = wasUnseen + }, + messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" }, + ) + + fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { + str1 = key + bool1 = wasUnseenWhenPinned + }, + messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" }, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 99327d1fe116..73ce48b2324a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -47,6 +47,7 @@ constructor( hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, keyguardCoordinator: KeyguardCoordinator, unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator, + lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator, rankingCoordinator: RankingCoordinator, colorizedFgsCoordinator: ColorizedFgsCoordinator, deviceProvisionedCoordinator: DeviceProvisionedCoordinator, @@ -87,7 +88,11 @@ constructor( mCoordinators.add(hideLocallyDismissedNotifsCoordinator) mCoordinators.add(hideNotifsForOtherUsersCoordinator) mCoordinators.add(keyguardCoordinator) - mCoordinators.add(unseenKeyguardCoordinator) + if (NotificationMinimalismPrototype.isEnabled) { + mCoordinators.add(lockScreenMinimalismCoordinator) + } else { + mCoordinators.add(unseenKeyguardCoordinator) + } mCoordinators.add(rankingCoordinator) mCoordinators.add(colorizedFgsCoordinator) mCoordinators.add(deviceProvisionedCoordinator) @@ -120,12 +125,12 @@ constructor( } // Manually add Ordered Sections - if (NotificationMinimalismPrototype.V2.isEnabled) { - mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing + if (NotificationMinimalismPrototype.isEnabled) { + mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing } mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp - if (NotificationMinimalismPrototype.V2.isEnabled) { - mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen + if (NotificationMinimalismPrototype.isEnabled) { + mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen } mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService if (PriorityPeopleSection.isEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt index 5dd1663f712f..5b25b117c761 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt @@ -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. @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.annotation.SuppressLint -import android.app.NotificationManager import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting @@ -30,21 +29,14 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.expansionChanges -import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype -import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING -import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.asIndenting @@ -73,9 +65,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.yield /** - * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section - * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the - * lockscreen. + * If the setting is enabled, this will track and hide seen notifications on the lockscreen. + * + * This is the "original" unseen keyguard coordinator because this is the logic originally developed + * for large screen devices where showing "seen" notifications on the lock screen was distracting. + * Moreover, this file was created during a project that will replace this logic, so the + * [LockScreenMinimalismCoordinator] is the expected replacement of this file. */ @CoordinatorScope @SuppressLint("SharedFlowCreation") @@ -100,10 +95,7 @@ constructor( private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { - if (NotificationMinimalismPrototype.V2.isEnabled) { - pipeline.addPromoter(unseenNotifPromoter) - pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) - } + NotificationMinimalismPrototype.assertInLegacyMode() pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) scope.launch { trackUnseenFilterSettingChanges() } @@ -112,6 +104,7 @@ constructor( private suspend fun trackSeenNotifications() { // Whether or not keyguard is visible (or occluded). + @Suppress("DEPRECATION") val isKeyguardPresentFlow: Flow<Boolean> = keyguardTransitionInteractor .transitionValue( @@ -265,11 +258,9 @@ constructor( } private fun unseenFeatureEnabled(): Flow<Boolean> { - if ( - NotificationMinimalismPrototype.V1.isEnabled || - NotificationMinimalismPrototype.V2.isEnabled - ) { - return flowOf(true) + if (NotificationMinimalismPrototype.isEnabled) { + // TODO(b/330387368): should this really just be turned off? If so, hide the setting. + return flowOf(false) } return secureSettings // emit whenever the setting has changed @@ -340,110 +331,18 @@ constructor( } } - private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return - // Only ever elevate a top unseen notification on keyguard, not even locked shade - if (statusBarStateController.state != StatusBarState.KEYGUARD) { - seenNotificationsInteractor.setTopOngoingNotification(null) - seenNotificationsInteractor.setTopUnseenNotification(null) - return - } - // On keyguard pick the top-ranked unseen or ongoing notification to elevate - val nonSummaryEntries: Sequence<NotificationEntry> = - list - .asSequence() - .flatMap { - when (it) { - is NotificationEntry -> listOfNotNull(it) - is GroupEntry -> it.children - else -> error("unhandled type of $it") - } - } - .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT } - seenNotificationsInteractor.setTopOngoingNotification( - nonSummaryEntries - .filter { ColorizedFgsCoordinator.isRichOngoing(it) } - .minByOrNull { it.ranking.rank } - ) - seenNotificationsInteractor.setTopUnseenNotification( - nonSummaryEntries - .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications } - .minByOrNull { it.ranking.rank } - ) - } - - @VisibleForTesting - val unseenNotifPromoter = - object : NotifPromoter("$TAG-unseen") { - override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false - else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false - else - seenNotificationsInteractor.isTopOngoingNotification(child) || - seenNotificationsInteractor.isTopUnseenNotification(child) - } - - val topOngoingSectioner = - object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { - override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false - return entry.anyEntry { notificationEntry -> - seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) - } - } - } - - val topUnseenSectioner = - object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { - override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false - return entry.anyEntry { notificationEntry -> - seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) - } - } - } - - private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) = - when { - predicate(representativeEntry) -> true - this !is GroupEntry -> false - else -> children.any(predicate) - } - @VisibleForTesting val unseenNotifFilter = - object : NotifFilter("$TAG-unseen") { + object : NotifFilter(TAG) { var hasFilteredAnyNotifs = false - /** - * Encapsulates a definition of "being on the keyguard". Note that these two definitions - * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does - * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing] - * is any state where the keyguard has not been dismissed, including locked shade and - * occluded lock screen. - * - * Returning false for locked shade and occluded states means that this filter will - * allow seen notifications to appear in the locked shade. - */ - private fun isOnKeyguard(): Boolean = - if (NotificationMinimalismPrototype.V2.isEnabled) { - false // disable this feature under this prototype - } else if ( - NotificationMinimalismPrototype.V1.isEnabled && - NotificationMinimalismPrototype.V1.showOnLockedShade - ) { - statusBarStateController.state == StatusBarState.KEYGUARD - } else { - keyguardRepository.isKeyguardShowing() - } - override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = when { // Don't apply filter if the setting is disabled !unseenFilterEnabled -> false // Don't apply filter if the keyguard isn't currently showing - !isOnKeyguard() -> false + !keyguardRepository.isKeyguardShowing() -> false // Don't apply the filter if the notification is unseen unseenNotifications.contains(entry) -> false // Don't apply the filter to (non-promoted) group summaries diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index caa6c17ac3d2..696298e82db9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; -import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -29,7 +27,10 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.GroupEntry; @@ -41,7 +42,6 @@ import com.android.systemui.statusbar.notification.collection.provider.VisualSta import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype; import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.util.Compile; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.kotlin.JavaAdapter; @@ -61,8 +61,6 @@ import javax.inject.Inject; // TODO(b/204468557): Move to @CoordinatorScope @SysUISingleton public class VisualStabilityCoordinator implements Coordinator, Dumpable { - public static final String TAG = "VisualStability"; - public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private final DelayableExecutor mDelayableExecutor; private final HeadsUpManager mHeadsUpManager; private final SeenNotificationsInteractor mSeenNotificationsInteractor; @@ -73,6 +71,8 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { private final VisualStabilityProvider mVisualStabilityProvider; private final WakefulnessLifecycle mWakefulnessLifecycle; private final CommunalInteractor mCommunalInteractor; + private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final VisualStabilityCoordinatorLogger mLogger; private boolean mSleepy = true; private boolean mFullyDozed; @@ -81,6 +81,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { private boolean mNotifPanelCollapsing; private boolean mNotifPanelLaunchingActivity; private boolean mCommunalShowing = false; + private boolean mLockscreenShowing = false; private boolean mPipelineRunAllowed; private boolean mReorderingAllowed; @@ -109,7 +110,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { VisibilityLocationProvider visibilityLocationProvider, VisualStabilityProvider visualStabilityProvider, WakefulnessLifecycle wakefulnessLifecycle, - CommunalInteractor communalInteractor) { + CommunalInteractor communalInteractor, + KeyguardTransitionInteractor keyguardTransitionInteractor, + VisualStabilityCoordinatorLogger logger) { mHeadsUpManager = headsUpManager; mShadeAnimationInteractor = shadeAnimationInteractor; mJavaAdapter = javaAdapter; @@ -120,6 +123,8 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { mStatusBarStateController = statusBarStateController; mDelayableExecutor = delayableExecutor; mCommunalInteractor = communalInteractor; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mLogger = logger; dumpManager.registerDumpable(this); } @@ -138,6 +143,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { this::onLaunchingActivityChanged); mJavaAdapter.alwaysCollectFlow(mCommunalInteractor.isIdleOnCommunal(), this::onCommunalShowingChanged); + mJavaAdapter.alwaysCollectFlow(mKeyguardTransitionInteractor.transitionValue( + KeyguardState.LOCKSCREEN), + this::onLockscreenKeyguardStateTransitionValueChanged); pipeline.setVisualStabilityManager(mNotifStabilityManager); } @@ -150,8 +158,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { if (entry == null) { return false; } - boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled() - && mSeenNotificationsInteractor.isTopUnseenNotification(entry); + boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled() + && (mSeenNotificationsInteractor.isTopUnseenNotification(entry) + || mSeenNotificationsInteractor.isTopOngoingNotification(entry)); if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) { return !mVisibilityLocationProvider.isInVisibleLocation(entry); } @@ -220,12 +229,12 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { boolean wasReorderingAllowed = mReorderingAllowed; mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity(); mReorderingAllowed = isReorderingAllowed(); - if (DEBUG && (wasPipelineRunAllowed != mPipelineRunAllowed - || wasReorderingAllowed != mReorderingAllowed)) { - Log.d(TAG, "Stability allowances changed:" - + " pipelineRunAllowed " + wasPipelineRunAllowed + "->" + mPipelineRunAllowed - + " reorderingAllowed " + wasReorderingAllowed + "->" + mReorderingAllowed - + " when setting " + field + "=" + value); + if (wasPipelineRunAllowed != mPipelineRunAllowed + || wasReorderingAllowed != mReorderingAllowed) { + mLogger.logAllowancesChanged( + wasPipelineRunAllowed, mPipelineRunAllowed, + wasReorderingAllowed, mReorderingAllowed, + field, value); } if (mPipelineRunAllowed && mIsSuppressingPipelineRun) { mNotifStabilityManager.invalidateList("pipeline run suppression ended"); @@ -250,7 +259,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { } private boolean isReorderingAllowed() { - return ((mFullyDozed && mSleepy) || !mPanelExpanded || mCommunalShowing) && !mPulsing; + final boolean sleepyAndDozed = mFullyDozed && mSleepy; + final boolean stackShowing = mPanelExpanded || mLockscreenShowing; + return (sleepyAndDozed || !stackShowing || mCommunalShowing) && !mPulsing; } /** @@ -363,4 +374,14 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { mCommunalShowing = isShowing; updateAllowedStates("communalShowing", isShowing); } + + private void onLockscreenKeyguardStateTransitionValueChanged(float value) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + return; + } + + final boolean isShowing = value > 0.0f; + mLockscreenShowing = isShowing; + updateAllowedStates("lockscreenShowing", isShowing); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt new file mode 100644 index 000000000000..fe23e4e12db4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.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.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.VisualStabilityLog +import javax.inject.Inject + +private const val TAG = "VisualStability" + +class VisualStabilityCoordinatorLogger +@Inject +constructor(@VisualStabilityLog private val buffer: LogBuffer) { + fun logAllowancesChanged( + wasRunAllowed: Boolean, + isRunAllowed: Boolean, + wasReorderingAllowed: Boolean, + isReorderingAllowed: Boolean, + field: String, + value: Boolean + ) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = wasRunAllowed + bool2 = isRunAllowed + bool3 = wasReorderingAllowed + bool4 = isReorderingAllowed + str1 = field + str2 = value.toString() + }, + { + "stability allowances changed:" + + " pipelineRunAllowed $bool1->$bool2" + + " reorderingAllowed $bool3->$bool4" + + " when setting $str1=$str2" + } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt index 5adf31b75fa7..5614f3fc9590 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt @@ -2,6 +2,7 @@ package com.android.systemui.statusbar.notification.collection.provider import android.util.ArraySet import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.util.ListenerSet import javax.inject.Inject @@ -13,12 +14,18 @@ class VisualStabilityProvider @Inject constructor() { /** The subset of active listeners which are temporary (will be removed after called) */ private val temporaryListeners = ArraySet<OnReorderingAllowedListener>() + private val banListeners = ListenerSet<OnReorderingBannedListener>() + var isReorderingAllowed = true set(value) { if (field != value) { field = value if (value) { notifyReorderingAllowed() + } else if (NotificationThrottleHun.isEnabled){ + banListeners.forEach { listener -> + listener.onReorderingBanned() + } } } } @@ -38,6 +45,10 @@ class VisualStabilityProvider @Inject constructor() { allListeners.addIfAbsent(listener) } + fun addPersistentReorderingBannedListener(listener: OnReorderingBannedListener) { + banListeners.addIfAbsent(listener) + } + /** Add a listener which will be removed when it is called. */ fun addTemporaryReorderingAllowedListener(listener: OnReorderingAllowedListener) { // Only add to the temporary set if it was added to the global set @@ -57,3 +68,7 @@ class VisualStabilityProvider @Inject constructor() { fun interface OnReorderingAllowedListener { fun onReorderingAllowed() } + +fun interface OnReorderingBannedListener { + fun onReorderingBanned() +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index bf44b9f3cf78..24b75d49ed16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -30,6 +30,7 @@ import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow 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 @@ -44,8 +45,17 @@ constructor( private val shadeInteractor: ShadeInteractor, ) { + /** The top-ranked heads up row, regardless of pinned state */ val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow + /** The top-ranked heads up row, if that row is pinned */ + val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> = + headsUpRepository.topHeadsUpRow + .flatMapLatest { repository -> + repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null) + } + .distinctUntilChanged() + /** Set of currently pinned top-level heads up rows to be displayed. */ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy { if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { @@ -89,10 +99,10 @@ constructor( flowOf(false) } else { combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { - hasPinnedRows, - animatingAway -> - hasPinnedRows || animatingAway - } + hasPinnedRows, + animatingAway -> + hasPinnedRows || animatingAway + } } } @@ -127,6 +137,9 @@ constructor( fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey + /** Returns the Notification Key (the standard string) of this row. */ + fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key + fun setHeadsUpAnimatingAway(animatingAway: Boolean) { headsUpRepository.setHeadsUpAnimatingAway(animatingAway) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt index 85c66bd6f25a..948a3c2f65b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.notification.domain.interactor +import android.util.IndentingPrintWriter import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.util.printSection import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow @@ -41,24 +43,42 @@ constructor( /** Set the entry that is identified as the top ongoing notification. */ fun setTopOngoingNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return notificationListRepository.topOngoingNotificationKey.value = entry?.key } /** Determine if the given notification is the top ongoing notification. */ fun isTopOngoingNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key /** Set the entry that is identified as the top unseen notification. */ fun setTopUnseenNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return notificationListRepository.topUnseenNotificationKey.value = entry?.key } /** Determine if the given notification is the top unseen notification. */ fun isTopUnseenNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key + + fun dump(pw: IndentingPrintWriter) = + with(pw) { + printSection("SeenNotificationsInteractor") { + print( + "hasFilteredOutSeenNotifications", + notificationListRepository.hasFilteredOutSeenNotifications.value + ) + print( + "topOngoingNotificationKey", + notificationListRepository.topOngoingNotificationKey.value + ) + print( + "topUnseenNotificationKey", + notificationListRepository.topUnseenNotificationKey.value + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index b8af3698fb63..fe86375d628e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -122,12 +122,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val timeRemaining = parseTimeDelta(remaining) TimerContentModel( icon = icon, - name = total, + // TODO: b/352142761 - define and use a string resource rather than " Timer". + // (The UX isn't final so using " Timer" for now). + name = total.replace("Σ", "") + " Timer", state = TimerContentModel.TimerState.Paused( timeRemaining = timeRemaining, - resumeIntent = notification.findActionWithName("Resume"), - resetIntent = notification.findActionWithName("Reset"), + resumeIntent = notification.findStartIntent(), + addMinuteAction = notification.findAddMinuteAction(), + resetAction = notification.findResetAction(), ) ) } @@ -136,12 +139,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis() TimerContentModel( icon = icon, - name = total, + // TODO: b/352142761 - define and use a string resource rather than " Timer". + // (The UX isn't final so using " Timer" for now). + name = total.replace("Σ", "") + " Timer", state = TimerContentModel.TimerState.Running( finishTime = finishTime, - pauseIntent = notification.findActionWithName("Pause"), - addOneMinuteIntent = notification.findActionWithName("Add 1 min"), + pauseIntent = notification.findPauseIntent(), + addMinuteAction = notification.findAddMinuteAction(), + resetAction = notification.findResetAction(), ) ) } @@ -149,8 +155,34 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : } } - private fun Notification.findActionWithName(name: String): PendingIntent? { - return actions.firstOrNull { name == it.title?.toString() }?.actionIntent + private fun Notification.findPauseIntent(): PendingIntent? { + return actions + .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true } + ?.actionIntent + } + + private fun Notification.findStartIntent(): PendingIntent? { + return actions + .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true } + ?.actionIntent + } + + // TODO: b/352142761 - switch to system attributes for label and icon. + // - We probably want a consistent look for the Reset button. (Double check with UX.) + // - Using the custom assets now since I couldn't an existing "Reset" icon. + private fun Notification.findResetAction(): Notification.Action? { + return actions.firstOrNull { + it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true + } + } + + // TODO: b/352142761 - check with UX on whether this should be required. + // - Alternative is to allow for optional actions in addition to main and reset. + // - For optional actions, we should take the custom label and icon. + private fun Notification.findAddMinuteAction(): Notification.Action? { + return actions.firstOrNull { + it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true + } } private fun parseCurrentTime(current: String): Long { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt index 558470175e8d..33b256456ca3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.shared +import android.app.Notification import android.app.PendingIntent import java.time.Duration @@ -32,6 +33,9 @@ data class TimerContentModel( ) : RichOngoingContentModel { /** The state (paused or running) of the timer, and relevant time */ sealed interface TimerState { + val addMinuteAction: Notification.Action? + val resetAction: Notification.Action? + /** * Indicates a running timer * @@ -41,7 +45,8 @@ data class TimerContentModel( data class Running( val finishTime: Long, val pauseIntent: PendingIntent?, - val addOneMinuteIntent: PendingIntent?, + override val addMinuteAction: Notification.Action?, + override val resetAction: Notification.Action?, ) : TimerState /** @@ -53,7 +58,8 @@ data class TimerContentModel( data class Paused( val timeRemaining: Duration, val resumeIntent: PendingIntent?, - val resetIntent: PendingIntent?, + override val addMinuteAction: Notification.Action?, + override val resetAction: Notification.Action?, ) : TimerState } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt index 0d83aced6d07..8c951877544c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt @@ -18,8 +18,9 @@ package com.android.systemui.statusbar.notification.row.ui.view import android.annotation.DrawableRes import android.content.Context +import android.graphics.BlendMode import android.util.AttributeSet -import android.widget.Button +import com.android.internal.widget.EmphasizedNotificationButton class TimerButtonView @JvmOverloads @@ -28,14 +29,19 @@ constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0, -) : Button(context, attrs, defStyleAttr, defStyleRes) { +) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) { private val Int.dp: Int get() = (this * context.resources.displayMetrics.density).toInt() fun setIcon(@DrawableRes icon: Int) { val drawable = context.getDrawable(icon) + + drawable?.mutate() + drawable?.setTintList(textColors) + drawable?.setTintBlendMode(BlendMode.SRC_IN) drawable?.setBounds(0, 0, 24.dp, 24.dp) + setCompoundDrawablesRelative(drawable, null, null, null) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt index 2e164d60431d..d481b50101c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.row.ui.view import android.content.Context -import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import android.os.SystemClock import android.util.AttributeSet import android.widget.Chronometer @@ -48,6 +48,9 @@ constructor( lateinit var altButton: TimerButtonView private set + lateinit var resetButton: TimerButtonView + private set + override fun onFinishInflate() { super.onFinishInflate() icon = requireViewById(R.id.icon) @@ -56,13 +59,14 @@ constructor( pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining) mainButton = requireViewById(R.id.mainButton) altButton = requireViewById(R.id.altButton) + resetButton = requireViewById(R.id.resetButton) } /** the resources configuration has changed such that the view needs to be reinflated */ fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange() - fun setIcon(iconDrawable: Drawable?) { - this.icon.setImageDrawable(iconDrawable) + fun setIcon(icon: Icon?) { + this.icon.setImageIcon(icon) } fun setLabel(label: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt index c9ff58961582..042d1bcfb2ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row.ui.viewbinder +import android.content.res.ColorStateList +import android.graphics.drawable.Icon import android.view.View import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope @@ -46,12 +48,43 @@ object TimerViewBinder { launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } } launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } } launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } } + launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } } } fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) { if (model != null) { - buttonView.setIcon(model.iconRes) - buttonView.setText(model.labelRes) + buttonView.setButtonBackground( + ColorStateList.valueOf( + buttonView.context.getColor(com.android.internal.R.color.system_accent2_100) + ) + ) + buttonView.setTextColor( + buttonView.context.getColor( + com.android.internal.R.color.notification_primary_text_color_light + ) + ) + + when (model) { + is TimerViewModel.ButtonViewModel.WithSystemAttrs -> { + buttonView.setIcon(model.iconRes) + buttonView.setText(model.labelRes) + } + is TimerViewModel.ButtonViewModel.WithCustomAttrs -> { + // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons + // with empty resPackage? RemoteViews handles this by using a different + // `contextForResources` for inflation. + val icon = + if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "") + Icon.createWithResource( + "com.google.android.deskclock", + model.icon.resId + ) + else model.icon + buttonView.setImageIcon(icon) + buttonView.text = model.label + } + } + buttonView.setOnClickListener( model.pendingIntent?.let { pendingIntent -> View.OnClickListener { pendingIntent.send() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt index a85c87f288d3..768a093e0b65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel import android.annotation.DrawableRes import android.annotation.StringRes import android.app.PendingIntent -import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag @@ -44,7 +44,7 @@ constructor( private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state } - val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable } + val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon } val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name } @@ -57,13 +57,13 @@ constructor( state.map { when (it) { is TimerState.Paused -> - ButtonViewModel( + ButtonViewModel.WithSystemAttrs( it.resumeIntent, com.android.systemui.res.R.string.controls_media_resume, // "Resume", com.android.systemui.res.R.drawable.ic_media_play ) is TimerState.Running -> - ButtonViewModel( + ButtonViewModel.WithSystemAttrs( it.pauseIntent, com.android.systemui.res.R.string.controls_media_button_pause, // "Pause", com.android.systemui.res.R.drawable.ic_media_pause @@ -73,31 +73,41 @@ constructor( val altButtonModel: Flow<ButtonViewModel?> = state.map { - when (it) { - is TimerState.Paused -> - it.resetIntent?.let { resetIntent -> - ButtonViewModel( - resetIntent, - com.android.systemui.res.R.string.reset, // "Reset", - com.android.systemui.res.R.drawable.ic_close_white_rounded - ) - } - is TimerState.Running -> - it.addOneMinuteIntent?.let { addOneMinuteIntent -> - ButtonViewModel( - addOneMinuteIntent, - com.android.systemui.res.R.string.add, // "Add 1 minute", - com.android.systemui.res.R.drawable.ic_add - ) - } + it.addMinuteAction?.let { action -> + ButtonViewModel.WithCustomAttrs( + action.actionIntent, + action.title, // "1:00", + action.getIcon() + ) + } + } + + val resetButtonModel: Flow<ButtonViewModel?> = + state.map { + it.resetAction?.let { action -> + ButtonViewModel.WithCustomAttrs( + action.actionIntent, + action.title, // "Reset", + action.getIcon() + ) } } - data class ButtonViewModel( - val pendingIntent: PendingIntent?, - @StringRes val labelRes: Int, - @DrawableRes val iconRes: Int, - ) + sealed interface ButtonViewModel { + val pendingIntent: PendingIntent? + + data class WithSystemAttrs( + override val pendingIntent: PendingIntent?, + @StringRes val labelRes: Int, + @DrawableRes val iconRes: Int, + ) : ButtonViewModel + + data class WithCustomAttrs( + override val pendingIntent: PendingIntent?, + val label: CharSequence, + val icon: Icon, + ) : ButtonViewModel + } } private fun Duration.format(): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt index bf37036ee018..06f3db504aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt @@ -24,102 +24,43 @@ import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the minimalism prototype flag state. */ @Suppress("NOTHING_TO_INLINE") object NotificationMinimalismPrototype { - - val version: Int by lazy { - SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2) - } - - object V1 { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the heads-up cycling animation enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.notificationMinimalismPrototype() && version == 1 - - /** - * the prototype will now show seen notifications on the locked shade by default, but this - * property read allows that to be quickly disabled for testing - */ - val showOnLockedShade: Boolean - get() = - if (isUnexpectedlyInLegacyMode()) false - else - SystemProperties.getBoolean( - "persist.notification_minimalism_prototype.show_on_locked_shade", - true - ) - - /** gets the configurable max number of notifications */ - val maxNotifs: Int - get() = - if (isUnexpectedlyInLegacyMode()) -1 - else - SystemProperties.getInt( - "persist.notification_minimalism_prototype.lock_screen_max_notifs", - 1 - ) - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an - * eng build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception - * if the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) - } - object V2 { - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the heads-up cycling animation enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.notificationMinimalismPrototype() && version == 2 - - /** - * The prototype will (by default) use a promoter to ensure that the top unseen notification - * is not grouped, but this property read allows that behavior to be disabled. - */ - val ungroupTopUnseen: Boolean - get() = - if (isUnexpectedlyInLegacyMode()) false - else - SystemProperties.getBoolean( - "persist.notification_minimalism_prototype.ungroup_top_unseen", - true - ) - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an - * eng build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception - * if the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) - } + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the heads-up cycling animation enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationMinimalismPrototype() + + /** + * The prototype will (by default) use a promoter to ensure that the top unseen notification is + * not grouped, but this property read allows that behavior to be disabled. + */ + val ungroupTopUnseen: Boolean + get() = + if (isUnexpectedlyInLegacyMode()) false + else + SystemProperties.getBoolean( + "persist.notification_minimalism_prototype.ungroup_top_unseen", + false + ) + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index cec1ef3f1e7b..5d2b61b42db9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -111,6 +111,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; @@ -4862,14 +4863,23 @@ public class NotificationStackScrollLayout * @param isHeadsUp true for appear, false for disappear animations */ public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { - final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); + boolean addAnimation = + mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); + if (NotificationThrottleHun.isEnabled()) { + final boolean closedAndSeenInShade = !mIsExpanded && row.getEntry() != null + && row.getEntry().isSeenInShade(); + addAnimation = addAnimation && !closedAndSeenInShade; + } if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation:" - + " willAdd=" + add - + " isHeadsUp=" + isHeadsUp - + " row=" + row.getEntry().getKey()); - } - if (add) { + + " addAnimation=" + addAnimation + + (row.getEntry() == null ? " entry NULL " + : " isSeenInShade=" + row.getEntry().isSeenInShade() + + " row=" + row.getEntry().getKey()) + + " mIsExpanded=" + mIsExpanded + + " isHeadsUp=" + isHeadsUp); + } + if (addAnimation) { // If we're hiding a HUN we just started showing THIS FRAME, then remove that event, // and do not add the disappear event either. if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 391bc43a784c..06222fdb2761 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -74,7 +74,7 @@ constructor( /** Whether we allow keyguard to show less important notifications above the shelf. */ private val limitLockScreenToOneImportant - get() = NotificationMinimalismPrototype.V2.isEnabled + get() = NotificationMinimalismPrototype.isEnabled /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */ private var dividerHeight by notNull<Float>() @@ -405,16 +405,8 @@ constructor( fun updateResources() { maxKeyguardNotifications = - infiniteIfNegative( - if (NotificationMinimalismPrototype.V1.isEnabled) { - NotificationMinimalismPrototype.V1.maxNotifs - } else { - resources.getInteger(R.integer.keyguard_max_notification_count) - } - ) - maxNotificationsExcludesMedia = - NotificationMinimalismPrototype.V1.isEnabled || - NotificationMinimalismPrototype.V2.isEnabled + infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) + maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled dividerHeight = max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 97266c57ba09..86c7c6bd1e86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -24,6 +24,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter.OnDismissAction +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy @@ -36,10 +37,16 @@ class ActivityStarterImpl constructor( private val statusBarStateController: SysuiStatusBarStateController, @Main private val mainExecutor: DelayableExecutor, + activityStarterInternal: Lazy<ActivityStarterInternalImpl>, legacyActivityStarter: Lazy<LegacyActivityStarterInternalImpl> ) : ActivityStarter { - private val activityStarterInternal: ActivityStarterInternal = legacyActivityStarter.get() + private val activityStarterInternal: ActivityStarterInternal = + if (SceneContainerFlag.isEnabled) { + activityStarterInternal.get() + } else { + legacyActivityStarter.get() + } override fun startPendingIntentDismissingKeyguard(intent: PendingIntent) { activityStarterInternal.startPendingIntentDismissingKeyguard( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt index ae98e1d60589..107bf1edb74f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt @@ -16,23 +16,93 @@ package com.android.systemui.statusbar.phone +import android.app.ActivityManager +import android.app.ActivityOptions +import android.app.ActivityTaskManager import android.app.PendingIntent +import android.app.TaskStackBuilder +import android.content.Context import android.content.Intent +import android.content.res.Resources import android.os.Bundle +import android.os.RemoteException import android.os.UserHandle +import android.provider.Settings +import android.util.Log +import android.view.RemoteAnimationAdapter import android.view.View +import android.view.WindowManager +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.DelegateTransitionAnimatorController +import com.android.systemui.assist.AssistManager +import com.android.systemui.camera.CameraIntents +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.DisplayId +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.ShadeController +import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.kotlin.getOrNull +import dagger.Lazy +import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi /** - * Encapsulates the activity logic for activity starter when flexiglass is enabled. + * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled. * * TODO: b/308819693 */ +@ExperimentalCoroutinesApi @SysUISingleton -class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInternal { +class ActivityStarterInternalImpl +@Inject +constructor( + private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>, + private val keyguardInteractor: KeyguardInteractor, + private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>, + private val context: Context, + @Main private val resources: Resources, + private val selectedUserInteractor: SelectedUserInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, + @DisplayId private val displayId: Int, + private val deviceProvisioningInteractor: DeviceProvisioningInteractor, + private val activityIntentHelper: ActivityIntentHelper, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val assistManagerLazy: Lazy<AssistManager>, + @Main private val mainExecutor: DelayableExecutor, + private val shadeControllerLazy: Lazy<ShadeController>, + private val communalSceneInteractor: CommunalSceneInteractor, + private val statusBarWindowController: StatusBarWindowController, + private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>, + private val shadeAnimationInteractor: ShadeAnimationInteractor, + private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>, + private val commandQueue: CommandQueue, + private val lockScreenUserManager: NotificationLockscreenUserManager, +) : ActivityStarterInternal { + private val centralSurfaces: CentralSurfaces? + get() = centralSurfacesOptLazy.get().getOrNull() + override fun startPendingIntentDismissingKeyguard( intent: PendingIntent, dismissShade: Boolean, @@ -45,7 +115,119 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna extraOptions: Bundle?, customMessage: String?, ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val animationController = + if (associatedView is ExpandableNotificationRow) { + centralSurfaces?.getAnimatorControllerFromNotification(associatedView) + } else animationController + + val willLaunchResolverActivity = + intent.isActivity && + activityIntentHelper.wouldPendingLaunchResolverActivity( + intent, + lockScreenUserManager.currentUserId, + ) + + val actuallyShowOverLockscreen = + showOverLockscreen && + intent.isActivity && + (skipLockscreenChecks || + activityIntentHelper.wouldPendingShowOverLockscreen( + intent, + lockScreenUserManager.currentUserId + )) + + val animate = + !willLaunchResolverActivity && + animationController != null && + shouldAnimateLaunch(intent.isActivity, actuallyShowOverLockscreen) + + // We wrap animationCallback with a StatusBarLaunchAnimatorController so + // that the shade is collapsed after the animation (or when it is cancelled, + // aborted, etc). + val statusBarController = + wrapAnimationControllerForShadeOrStatusBar( + animationController = animationController, + dismissShade = dismissShade, + isLaunchForActivity = intent.isActivity, + ) + val controller = + if (actuallyShowOverLockscreen) { + wrapAnimationControllerForLockscreen(dismissShade, statusBarController) + } else { + statusBarController + } + + // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we + // run the animation on the keyguard). The animation will take care of (instantly) + // collapsing the shade and hiding the keyguard once it is done. + val collapse = dismissShade && !animate + val runnable = Runnable { + try { + activityTransitionAnimator.startPendingIntentWithAnimation( + controller, + animate, + intent.creatorPackage, + actuallyShowOverLockscreen, + object : ActivityTransitionAnimator.PendingIntentStarter { + override fun startPendingIntent( + animationAdapter: RemoteAnimationAdapter? + ): Int { + val options = + ActivityOptions( + CentralSurfaces.getActivityOptions(displayId, animationAdapter) + .apply { extraOptions?.let { putAll(it) } } + ) + // TODO b/221255671: restrict this to only be set for notifications + options.isEligibleForLegacyPermissionPrompt = true + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + return intent.sendAndReturnResult( + context, + 0, + fillInIntent, + null, + null, + null, + options.toBundle() + ) + } + }, + ) + } catch (e: PendingIntent.CanceledException) { + // the stack trace isn't very helpful here. + // Just log the exception message. + Log.w(TAG, "Sending intent failed: $e") + if (!collapse) { + // executeRunnableDismissingKeyguard did not collapse for us already. + shadeControllerLazy.get().collapseOnMainThread() + } + // TODO: Dismiss Keyguard. + } + if (intent.isActivity) { + assistManagerLazy.get().hideAssist() + // This activity could have started while the device is dreaming, in which case + // the dream would occlude the activity. In order to show the newly started + // activity, we wake from the dream. + centralSurfaces?.awakenDreams() + } + intentSentUiThreadCallback?.let { mainExecutor.execute(it) } + } + + if (!actuallyShowOverLockscreen) { + mainExecutor.execute { + executeRunnableDismissingKeyguard( + runnable = runnable, + afterKeyguardGone = willLaunchResolverActivity, + dismissShade = collapse, + willAnimateOnKeyguard = animate, + customMessage = customMessage, + ) + } + } else { + mainExecutor.execute(runnable) + } } override fun startActivityDismissingKeyguard( @@ -59,7 +241,116 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna disallowEnterPictureInPictureWhileLaunching: Boolean, userHandle: UserHandle? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val userHandle: UserHandle = userHandle ?: getActivityUserHandle(intent) + + if (onlyProvisioned && !deviceProvisioningInteractor.isDeviceProvisioned()) return + + val willLaunchResolverActivity: Boolean = + activityIntentHelper.wouldLaunchResolverActivity( + intent, + selectedUserInteractor.getSelectedUserId(), + ) + + val animate = + animationController != null && + !willLaunchResolverActivity && + shouldAnimateLaunch(isActivityIntent = true) + val animController = + wrapAnimationControllerForShadeOrStatusBar( + animationController = animationController, + dismissShade = dismissShade, + isLaunchForActivity = true, + ) + + // If we animate, we will dismiss the shade only once the animation is done. This is + // taken care of by the StatusBarLaunchAnimationController. + val dismissShadeDirectly = dismissShade && animController == null + + val runnable = Runnable { + assistManagerLazy.get().hideAssist() + intent.flags = + if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) { + Intent.FLAG_ACTIVITY_NEW_TASK + } else { + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + intent.addFlags(flags) + val result = intArrayOf(ActivityManager.START_CANCELED) + activityTransitionAnimator.startIntentWithAnimation( + animController, + animate, + intent.getPackage() + ) { adapter: RemoteAnimationAdapter? -> + val options = + ActivityOptions(CentralSurfaces.getActivityOptions(displayId, adapter)) + + // We know that the intent of the caller is to dismiss the keyguard and + // this runnable is called right after the keyguard is solved, so we tell + // WM that we should dismiss it to avoid flickers when opening an activity + // that can also be shown over the keyguard. + options.setDismissKeyguardIfInsecure() + options.setDisallowEnterPictureInPictureWhileLaunching( + disallowEnterPictureInPictureWhileLaunching + ) + if (CameraIntents.isInsecureCameraIntent(intent)) { + // Normally an activity will set it's requested rotation + // animation on its window. However when launching an activity + // causes the orientation to change this is too late. In these cases + // the default animation is used. This doesn't look good for + // the camera (as it rotates the camera contents out of sync + // with physical reality). So, we ask the WindowManager to + // force the cross fade animation if an orientation change + // happens to occur during the launch. + options.rotationAnimationHint = + WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS + } + if (Settings.Panel.ACTION_VOLUME == intent.action) { + // Settings Panel is implemented as activity(not a dialog), so + // underlying app is paused and may enter picture-in-picture mode + // as a result. + // So we need to disable picture-in-picture mode here + // if it is volume panel. + options.setDisallowEnterPictureInPictureWhileLaunching(true) + } + try { + result[0] = + ActivityTaskManager.getService() + .startActivityAsUser( + null, + context.basePackageName, + context.attributionTag, + intent, + intent.resolveTypeIfNeeded(context.contentResolver), + null, + null, + 0, + Intent.FLAG_ACTIVITY_NEW_TASK, + null, + options.toBundle(), + userHandle.identifier, + ) + } catch (e: RemoteException) { + Log.w(TAG, "Unable to start activity", e) + } + result[0] + } + callback?.onActivityStarted(result[0]) + } + val cancelRunnable = Runnable { + callback?.onActivityStarted(ActivityManager.START_CANCELED) + } + // Do not deferKeyguard when occluded because, when keyguard is occluded, + // we do not launch the activity until keyguard is done. + executeRunnableDismissingKeyguard( + runnable, + cancelRunnable, + dismissShadeDirectly, + willLaunchResolverActivity, + deferred = !isKeyguardOccluded(), + animate, + customMessage, + ) } override fun startActivity( @@ -69,7 +360,64 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna showOverLockscreenWhenLocked: Boolean, userHandle: UserHandle? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val userHandle = userHandle ?: getActivityUserHandle(intent) + // Make sure that we dismiss the keyguard if it is directly dismissible or when we don't + // want to show the activity above it. + if (deviceEntryInteractor.isUnlocked.value || !showOverLockscreenWhenLocked) { + startActivityDismissingKeyguard( + intent = intent, + onlyProvisioned = false, + dismissShade = dismissShade, + disallowEnterPictureInPictureWhileLaunching = false, + callback = null, + flags = 0, + animationController = animationController, + userHandle = userHandle, + ) + return + } + + val animate = + animationController != null && + shouldAnimateLaunch( + isActivityIntent = true, + showOverLockscreen = showOverLockscreenWhenLocked + ) + + var controller: ActivityTransitionAnimator.Controller? = null + if (animate) { + // Wrap the animation controller to dismiss the shade and set + // mIsLaunchingActivityOverLockscreen during the animation. + val delegate = + wrapAnimationControllerForShadeOrStatusBar( + animationController = animationController, + dismissShade = dismissShade, + isLaunchForActivity = true, + ) + controller = wrapAnimationControllerForLockscreen(dismissShade, delegate) + } else if (dismissShade) { + // The animation will take care of dismissing the shade at the end of the animation. + // If we don't animate, collapse it directly. + shadeControllerLazy.get().cancelExpansionAndCollapseShade() + } + + // We should exit the dream to prevent the activity from starting below the + // dream. + if (keyguardInteractor.isDreaming.value) { + centralSurfaces?.awakenDreams() + } + + activityTransitionAnimator.startIntentWithAnimation( + controller, + animate, + intent.getPackage(), + showOverLockscreenWhenLocked + ) { adapter: RemoteAnimationAdapter? -> + TaskStackBuilder.create(context) + .addNextIntent(intent) + .startActivities(CentralSurfaces.getActivityOptions(displayId, adapter), userHandle) + } } override fun dismissKeyguardThenExecute( @@ -78,7 +426,23 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna afterKeyguardGone: Boolean, customMessage: String? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone") + + // TODO b/308819693: startWakeAndUnlock animation when pulsing + + if (isKeyguardShowing()) { + statusBarKeyguardViewManagerLazy + .get() + .dismissWithAction(action, cancel, afterKeyguardGone, customMessage) + } else { + // If the keyguard isn't showing but the device is dreaming, we should exit the + // dream. + if (keyguardInteractor.isDreaming.value) { + centralSurfaces?.awakenDreams() + } + action.onDismiss() + } } override fun executeRunnableDismissingKeyguard( @@ -90,10 +454,195 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna willAnimateOnKeyguard: Boolean, customMessage: String? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val onDismissAction: ActivityStarter.OnDismissAction = + object : ActivityStarter.OnDismissAction { + override fun onDismiss(): Boolean { + if (runnable != null) { + if (isKeyguardOccluded()) { + statusBarKeyguardViewManagerLazy + .get() + .addAfterKeyguardGoneRunnable(runnable) + } else { + mainExecutor.execute(runnable) + } + } + if (dismissShade) { + shadeControllerLazy.get().collapseShadeForActivityStart() + } + if (Flags.communalHub()) { + communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard() + } + return deferred + } + + override fun willRunAnimationOnKeyguard(): Boolean { + if (Flags.communalHub() && communalSceneInteractor.isIdleOnCommunal.value) { + // Override to false when launching activity over the hub that requires auth + return false + } + return willAnimateOnKeyguard + } + } + dismissKeyguardThenExecute( + onDismissAction, + cancelAction, + afterKeyguardGone, + customMessage, + ) } override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean { - TODO("Not yet implemented b/308819693") + return shouldAnimateLaunch(isActivityIntent, false) + } + + /** + * Whether we should animate an activity launch. + * + * Note: This method must be called *before* dismissing the keyguard. + */ + private fun shouldAnimateLaunch( + isActivityIntent: Boolean, + showOverLockscreen: Boolean, + ): Boolean { + // TODO(b/294418322): always support launch animations when occluded. + val ignoreOcclusion = showOverLockscreen && Flags.mediaLockscreenLaunchAnimation() + if (isKeyguardOccluded() && !ignoreOcclusion) { + return false + } + + // Always animate if we are not showing the keyguard or if we animate over the lockscreen + // (without unlocking it). + if (showOverLockscreen || !isKeyguardShowing()) { + return true + } + + // We don't animate non-activity launches as they can break the animation. + // TODO(b/184121838): Support non activity launches on the lockscreen. + return isActivityIntent + } + + /** Retrieves the current user handle to start the Activity. */ + private fun getActivityUserHandle(intent: Intent): UserHandle { + val packages: Array<String> = resources.getStringArray(R.array.system_ui_packages) + for (pkg in packages) { + val componentName = intent.component ?: break + if (pkg == componentName.packageName) { + return UserHandle(UserHandle.myUserId()) + } + } + return UserHandle(selectedUserInteractor.getSelectedUserId()) + } + + private fun isKeyguardShowing(): Boolean { + return !deviceEntryInteractor.isDeviceEntered.value + } + + private fun isKeyguardOccluded(): Boolean { + return keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED + } + + /** + * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that: + * - if it launches in the notification shade window and `dismissShade` is true, then the shade + * will be instantly dismissed at the end of the animation. + * - if it launches in status bar window, it will make the status bar window match the device + * size during the animation (that way, the animation won't be clipped by the status bar + * size). + * + * @param animationController the controller that is wrapped and will drive the main animation. + * @param dismissShade whether the notification shade will be dismissed at the end of the + * animation. This is ignored if `animationController` is not animating in the shade window. + * @param isLaunchForActivity whether the launch is for an activity. + */ + private fun wrapAnimationControllerForShadeOrStatusBar( + animationController: ActivityTransitionAnimator.Controller?, + dismissShade: Boolean, + isLaunchForActivity: Boolean, + ): ActivityTransitionAnimator.Controller? { + if (animationController == null) { + return null + } + val rootView = animationController.transitionContainer.rootView + val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> = + statusBarWindowController.wrapAnimationControllerIfInStatusBar( + rootView, + animationController + ) + if (controllerFromStatusBar.isPresent) { + return controllerFromStatusBar.get() + } + + centralSurfaces?.let { + // If the view is not in the status bar, then we are animating a view in the shade. + // We have to make sure that we collapse it when the animation ends or is cancelled. + if (dismissShade) { + return StatusBarTransitionAnimatorController( + animationController, + shadeAnimationInteractor, + shadeControllerLazy.get(), + notifShadeWindowControllerLazy.get(), + commandQueue, + displayId, + isLaunchForActivity + ) + } + } + + return animationController + } + + /** + * Wraps an animation controller so that if an activity would be launched on top of the + * lockscreen, the correct flags are set for it to be occluded. + */ + private fun wrapAnimationControllerForLockscreen( + dismissShade: Boolean, + animationController: ActivityTransitionAnimator.Controller? + ): ActivityTransitionAnimator.Controller? { + return animationController?.let { + object : DelegateTransitionAnimatorController(it) { + override fun onIntentStarted(willAnimate: Boolean) { + delegate.onIntentStarted(willAnimate) + if (willAnimate) { + centralSurfaces?.setIsLaunchingActivityOverLockscreen(true, dismissShade) + } + } + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + super.onTransitionAnimationStart(isExpandingFullyAbove) + if (Flags.communalHub()) { + communalSceneInteractor.snapToScene( + CommunalScenes.Blank, + ActivityTransitionAnimator.TIMINGS.totalDuration + ) + } + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + // Set mIsLaunchingActivityOverLockscreen to false before actually + // finishing the animation so that we can assume that + // mIsLaunchingActivityOverLockscreen being true means that we will + // collapse the shade (or at least run the post collapse runnables) + // later on. + centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false) + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) + } + + override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { + // Set mIsLaunchingActivityOverLockscreen to false before actually + // finishing the animation so that we can assume that + // mIsLaunchingActivityOverLockscreen being true means that we will + // collapse the shade (or at least run the // post collapse + // runnables) later on. + centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false) + delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) + } + } + } + } + + companion object { + private const val TAG = "ActivityStarterInternalImpl" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index a2e44dffb767..e08dbb9df7dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -39,11 +39,13 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.AnimationStateHandler; @@ -86,7 +88,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); private final VisualStabilityProvider mVisualStabilityProvider; - private final AvalancheController mAvalancheController; + private AvalancheController mAvalancheController; // TODO(b/328393698) move the topHeadsUpRow logic to an interactor private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = @@ -173,6 +175,12 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements }); javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded); + if (NotificationThrottleHun.isEnabled()) { + mVisualStabilityProvider.addPersistentReorderingBannedListener( + mOnReorderingBannedListener); + mVisualStabilityProvider.addPersistentReorderingAllowedListener( + mOnReorderingAllowedListener); + } } public void setAnimationStateHandler(AnimationStateHandler handler) { @@ -300,6 +308,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey()); if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) { headsUpEntry.mRemoteInputActive = remoteInputActive; + if (ExpandHeadsUpOnInlineReply.isEnabled() && remoteInputActive) { + headsUpEntry.mRemoteInputActivatedAtLeastOnce = true; + } if (remoteInputActive) { headsUpEntry.cancelAutoRemovalCallbacks("setRemoteInputActive(true)"); } else { @@ -379,6 +390,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); + if (NotificationThrottleHun.isEnabled()) { + mAvalancheController.setEnableAtRuntime(true); + } for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already @@ -389,6 +403,22 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); }; + private final OnReorderingBannedListener mOnReorderingBannedListener = () -> { + if (mAvalancheController != null) { + // In open shade the first HUN is pinned, and visual stability logic prevents us from + // unpinning this first HUN as long as the shade remains open. AvalancheController only + // shows the next HUN when the currently showing HUN is unpinned, so we must disable + // throttling here so that the incoming HUN stream is not forever paused. This is reset + // when reorder becomes allowed. + mAvalancheController.setEnableAtRuntime(false); + + // Note that we cannot do the above when + // 1) The remove runnable runs because its delay means it may not run before shade close + // 2) Reordering is allowed again (when shade closes) because the HUN appear animation + // will have started by then + } + }; + /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpManager utility (protected) methods overrides: @@ -561,18 +591,36 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements } @Override - protected Runnable createRemoveRunnable(NotificationEntry entry) { - return () -> { + protected void setEntry(@androidx.annotation.NonNull NotificationEntry entry, + @androidx.annotation.Nullable Runnable removeRunnable) { + super.setEntry(entry, removeRunnable); + + if (NotificationThrottleHun.isEnabled()) { if (!mVisualStabilityProvider.isReorderingAllowed() // We don't want to allow reordering while pulsing, but headsup need to // time out anyway && !entry.showingPulsing()) { mEntriesToRemoveWhenReorderingAllowed.add(entry); + entry.setSeenInShade(true); + } + } + } + + @Override + protected Runnable createRemoveRunnable(NotificationEntry entry) { + return () -> { + if (!NotificationThrottleHun.isEnabled() + && !mVisualStabilityProvider.isReorderingAllowed() + // We don't want to allow reordering while pulsing, but headsup need to + // time out anyway + && !entry.showingPulsing()) { + mEntriesToRemoveWhenReorderingAllowed.add(entry); mVisualStabilityProvider.addTemporaryReorderingAllowedListener( mOnReorderingAllowedListener); } else if (mTrackingHeadsUp) { mEntriesToRemoveAfterExpand.add(entry); - } else { + } else if (mVisualStabilityProvider.isReorderingAllowed() + || entry.showingPulsing()) { removeEntry(entry.getKey(), "createRemoveRunnable"); } }; @@ -585,8 +633,10 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements if (mEntriesToRemoveAfterExpand.contains(mEntry)) { mEntriesToRemoveAfterExpand.remove(mEntry); } - if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { - mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); + if (!NotificationThrottleHun.isEnabled()) { + if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { + mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt index e69a78fb14fd..1a4708170a05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt @@ -40,7 +40,6 @@ import com.android.systemui.assist.AssistManager import com.android.systemui.camera.CameraIntents import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor -import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.DisplayId import com.android.systemui.dagger.qualifiers.Main @@ -208,10 +207,16 @@ constructor( val cancelRunnable = Runnable { callback?.onActivityStarted(ActivityManager.START_CANCELED) } - // Do not deferKeyguard when occluded because, when keyguard is occluded, - // we do not launch the activity until keyguard is done. + // Do not deferKeyguard when occluded because, when keyguard is occluded, we do not launch + // the activity until keyguard is done. The only exception is when we're on the Hub and want + // to dismiss the shade immediately, which means that another animation will take care of + // the transition. val occluded = (keyguardStateController.isShowing && keyguardStateController.isOccluded) - val deferred = !occluded + val dismissOnCommunal = + communalSettingsInteractor.isCommunalFlagEnabled() && + communalSceneInteractor.isCommunalVisible.value && + dismissShadeDirectly + val deferred = !occluded || dismissOnCommunal executeRunnableDismissingKeyguard( runnable, cancelRunnable, @@ -463,10 +468,18 @@ constructor( object : ActivityStarter.OnDismissAction { override fun onDismiss(): Boolean { if (runnable != null) { + // We don't wait for Keyguard to be gone if we're dismissing the shade + // immediately and we're on the Communal Hub. This is to make sure that the + // Hub -> Edit Mode transition is seamless. + val dismissOnCommunal = + communalSettingsInteractor.isCommunalFlagEnabled() && + communalSceneInteractor.isCommunalVisible.value && + dismissShade if ( keyguardStateController.isShowing && keyguardStateController.isOccluded && - !isCommunalWidgetLaunch() + !isCommunalWidgetLaunch() && + !dismissOnCommunal ) { statusBarKeyguardViewManagerLazy .get() @@ -562,12 +575,6 @@ constructor( override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { super.onTransitionAnimationStart(isExpandingFullyAbove) - if (communalSettingsInteractor.isCommunalFlagEnabled()) { - communalSceneInteractor.snapToScene( - CommunalScenes.Blank, - ActivityTransitionAnimator.TIMINGS.totalDuration - ) - } // Double check that the keyguard is still showing and not going // away, but if so set the keyguard occluded. Typically, WM will let // KeyguardViewMediator know directly, but we're overriding that to diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt index 4c97854bb5c9..16bd7f830c66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt @@ -110,7 +110,7 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa chipView.setOnClickListener(chipModel.onClickListener) // Accessibility - setChipAccessibility(chipModel, chipView) + setChipAccessibility(chipModel, chipView, chipBackgroundView) // Colors val textColor = chipModel.colors.text(chipContext) @@ -213,7 +213,11 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom) } - private fun setChipAccessibility(chipModel: OngoingActivityChipModel.Shown, chipView: View) { + private fun setChipAccessibility( + chipModel: OngoingActivityChipModel.Shown, + chipView: View, + chipBackgroundView: View, + ) { when (chipModel) { is OngoingActivityChipModel.Shown.Countdown -> { // Set as assertive so talkback will announce the countdown @@ -224,6 +228,16 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE } } + // Clickable chips need to be a minimum size for accessibility purposes, but let + // non-clickable chips be smaller. + if (chipModel.onClickListener != null) { + chipBackgroundView.minimumWidth = + chipBackgroundView.context.resources.getDimensionPixelSize( + R.dimen.min_clickable_item_size + ) + } else { + chipBackgroundView.minimumWidth = 0 + } } private fun animateLightsOutView(view: View, visible: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index 40799583a7b9..a88c6d707da4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -37,13 +37,27 @@ import javax.inject.Inject @SysUISingleton class AvalancheController @Inject -constructor(dumpManager: DumpManager, - private val uiEventLogger: UiEventLogger, - @Background private val bgHandler: Handler +constructor( + dumpManager: DumpManager, + private val uiEventLogger: UiEventLogger, + @Background private val bgHandler: Handler ) : Dumpable { private val tag = "AvalancheController" private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) + var enableAtRuntime = true + set(value) { + if (!value) { + // Waiting HUNs in AvalancheController are shown in the HUN section in open shade. + // Clear them so we don't show them again when the shade closes and reordering is + // allowed again. + logDroppedHunsInBackground(getWaitingKeys().size) + clearNext() + } + if (field != value) { + field = value + } + } // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null @@ -90,13 +104,17 @@ constructor(dumpManager: DumpManager, return getKey(headsUpEntryShowing) } + fun isEnabled(): Boolean { + return NotificationThrottleHun.isEnabled && enableAtRuntime + } + /** Run or delay Runnable for given HeadsUpEntry */ fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) { if (runnable == null) { log { "Runnable is NULL, stop update." } return } - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { runnable.run() return } @@ -156,7 +174,7 @@ constructor(dumpManager: DumpManager, log { "Runnable is NULL, stop delete." } return } - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { runnable.run() return } @@ -185,7 +203,8 @@ constructor(dumpManager: DumpManager, showNext() runnable.run() } else { - log { "$fn => removing untracked ${getKey(entry)}" } + log { "$fn => run runnable for untracked shown ${getKey(entry)}" } + runnable.run() } logState("after $fn") } @@ -197,7 +216,7 @@ constructor(dumpManager: DumpManager, * BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration. */ fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { // Use default duration, like we did before AvalancheController existed return autoDismissMs } @@ -246,7 +265,7 @@ constructor(dumpManager: DumpManager, /** Return true if entry is waiting to show. */ fun isWaiting(key: String): Boolean { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return false } for (entry in nextMap.keys) { @@ -259,7 +278,7 @@ constructor(dumpManager: DumpManager, /** Return list of keys for huns waiting */ fun getWaitingKeys(): MutableList<String> { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return mutableListOf() } val keyList = mutableListOf<String>() @@ -270,7 +289,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntry(key: String): HeadsUpEntry? { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return null } for (headsUpEntry in nextMap.keys) { @@ -282,7 +301,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntryList(): List<HeadsUpEntry> { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return mutableListOf() } return nextMap.keys.toList() @@ -340,13 +359,15 @@ constructor(dumpManager: DumpManager, showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList) } - fun logDroppedHunsInBackground(numDropped: Int) { - bgHandler.post(Runnable { - // Do this in the background to avoid missing frames when closing the shade - for (n in 1..numDropped) { - uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED) + private fun logDroppedHunsInBackground(numDropped: Int) { + bgHandler.post( + Runnable { + // Do this in the background to avoid missing frames when closing the shade + for (n in 1..numDropped) { + uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED) + } } - }) + ) } fun clearNext() { @@ -367,7 +388,8 @@ constructor(dumpManager: DumpManager, "\nPREVIOUS: [$previousHunKey]" + "\nNEXT LIST: $nextListStr" + "\nNEXT MAP: $nextMapStr" + - "\nDROPPED: $dropSetStr" + "\nDROPPED: $dropSetStr" + + "\nENABLED: $enableAtRuntime" } private fun logState(reason: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 220e729625af..65171358b050 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; +import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.util.ListenerSet; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.settings.GlobalSettings; @@ -726,6 +727,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * of AvalancheController that take it as param. */ public class HeadsUpEntry implements Comparable<HeadsUpEntry> { + public boolean mRemoteInputActivatedAtLeastOnce; public boolean mRemoteInputActive; public boolean mUserActionMayIndirectlyRemove; @@ -756,7 +758,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { setEntry(entry, createRemoveRunnable(entry)); } - private void setEntry(@NonNull final NotificationEntry entry, + protected void setEntry(@NonNull final NotificationEntry entry, @Nullable Runnable removeRunnable) { mEntry = entry; mRemoveRunnable = removeRunnable; @@ -835,6 +837,15 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ public boolean isSticky() { if (mEntry == null) return false; + + if (ExpandHeadsUpOnInlineReply.isEnabled()) { + // we don't consider pinned and expanded huns as sticky after the remote input + // has been activated for them + if (!mRemoteInputActive && mRemoteInputActivatedAtLeastOnce) { + return false; + } + } + return (mEntry.isRowPinned() && mExpanded) || mRemoteInputActive || hasFullScreenIntent(mEntry); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt index 483855409b91..07bbca74e12e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt @@ -31,6 +31,13 @@ interface DeviceProvisioningRepository { * @see android.provider.Settings.Global.DEVICE_PROVISIONED */ val isDeviceProvisioned: Flow<Boolean> + + /** + * Whether this device has been provisioned. + * + * @see android.provider.Settings.Global.DEVICE_PROVISIONED + */ + fun isDeviceProvisioned(): Boolean } @Module @@ -48,11 +55,15 @@ constructor( val listener = object : DeviceProvisionedController.DeviceProvisionedListener { override fun onDeviceProvisionedChanged() { - trySend(deviceProvisionedController.isDeviceProvisioned) + trySend(isDeviceProvisioned()) } } deviceProvisionedController.addCallback(listener) - trySend(deviceProvisionedController.isDeviceProvisioned) + trySend(isDeviceProvisioned()) awaitClose { deviceProvisionedController.removeCallback(listener) } } + + override fun isDeviceProvisioned(): Boolean { + return deviceProvisionedController.isDeviceProvisioned + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt index 66ed092fcce1..ace4ce0673a4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.Flow class DeviceProvisioningInteractor @Inject constructor( - repository: DeviceProvisioningRepository, + private val repository: DeviceProvisioningRepository, ) { /** * Whether this device has been provisioned. @@ -34,4 +34,8 @@ constructor( * @see android.provider.Settings.Global.DEVICE_PROVISIONED */ val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned + + fun isDeviceProvisioned(): Boolean { + return repository.isDeviceProvisioned() + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index c45f98e5f4f5..066bfc5c588d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -18,6 +18,8 @@ package com.android.systemui.volume; import static android.media.AudioManager.RINGER_MODE_NORMAL; +import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; + import android.app.ActivityManager; import android.app.KeyguardManager; import android.app.NotificationManager; @@ -59,6 +61,8 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; @@ -76,6 +80,8 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor; import dalvik.annotation.optimization.NeverCompile; @@ -102,7 +108,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000; - private static final int DYNAMIC_STREAM_START_INDEX = 100; + // We only need one dynamic stream for broadcast because at most two headsets are allowed + // to join local broadcast in current stage. + // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio + // streams defined in AudioSystem for now and audio team is in the middle of restructure, + // no new default stream is preferred. + @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99; + private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100; private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -145,6 +157,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final State mState = new State(); protected final MediaSessionsCallbacks mMediaSessionsCallbacksW; private final VibratorHelper mVibrator; + private final AudioSharingInteractor mAudioSharingInteractor; + private final JavaAdapter mJavaAdapter; private final boolean mHasVibrator; private boolean mShowA11yStream; private boolean mShowVolumeDialog; @@ -188,7 +202,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa KeyguardManager keyguardManager, ActivityManager activityManager, UserTracker userTracker, - DumpManager dumpManager + DumpManager dumpManager, + AudioSharingInteractor audioSharingInteractor, + JavaAdapter javaAdapter ) { mContext = context.getApplicationContext(); mPackageManager = packageManager; @@ -200,6 +216,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mRouter2Manager = MediaRouter2Manager.getInstance(mContext); mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext); mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW); + mAudioSharingInteractor = audioSharingInteractor; + mJavaAdapter = javaAdapter; mAudio = audioManager; mNoMan = notificationManager; mObserver = new SettingObserver(mWorker); @@ -272,6 +290,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } catch (SecurityException e) { Log.w(TAG, "No access to media sessions", e); } + if (volumeDialogAudioSharingFix()) { + Slog.d(TAG, "Start collect volume changes in audio sharing"); + mJavaAdapter.alwaysCollectFlow( + mAudioSharingInteractor.getVolume(), + this::handleAudioSharingStreamVolumeChanges); + } } public void setVolumePolicy(VolumePolicy policy) { @@ -545,7 +569,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mState.activeStream = activeStream; Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream); if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream); - final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1; + final int s = + activeStream + < (volumeDialogAudioSharingFix() + ? DYNAMIC_STREAM_BROADCAST + : DYNAMIC_STREAM_REMOTE_START_INDEX) + ? activeStream + : -1; if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s); mAudio.forceVolumeControlStream(s); return true; @@ -726,7 +756,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private void onSetStreamVolumeW(int stream, int level) { if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level); - if (stream >= DYNAMIC_STREAM_START_INDEX) { + if (volumeDialogAudioSharingFix() && stream == DYNAMIC_STREAM_BROADCAST) { + Slog.d(TAG, "onSetStreamVolumeW set broadcast stream level = " + level); + mAudioSharingInteractor.setStreamVolume(level); + return; + } + if (stream >= DYNAMIC_STREAM_REMOTE_START_INDEX) { mMediaSessionsCallbacksW.setStreamVolume(stream, level); return; } @@ -758,6 +793,40 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa DndTile.setVisible(mContext, true); } + void handleAudioSharingStreamVolumeChanges(@Nullable Integer volume) { + if (volume == null) { + if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) { + mState.states.remove(DYNAMIC_STREAM_BROADCAST); + Slog.d(TAG, "Remove audio sharing stream"); + mCallbacks.onStateChanged(mState); + } + } else { + if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) { + StreamState ss = mState.states.get(DYNAMIC_STREAM_BROADCAST); + if (ss.level != volume) { + ss.level = volume; + Slog.d(TAG, "updateState, audio sharing stream volume = " + volume); + mCallbacks.onStateChanged(mState); + } + } else { + StreamState ss = streamStateW(DYNAMIC_STREAM_BROADCAST); + ss.dynamic = true; + ss.levelMin = mAudioSharingInteractor.getVolumeMin(); + ss.levelMax = mAudioSharingInteractor.getVolumeMax(); + if (ss.level != volume) { + ss.level = volume; + } + String label = mContext.getString(R.string.audio_sharing_description); + if (!Objects.equals(ss.remoteLabel, label)) { + ss.name = -1; + ss.remoteLabel = label; + } + Slog.d(TAG, "updateState, new audio sharing stream volume = " + volume); + mCallbacks.onStateChanged(mState); + } + } + } + private final class VC extends IVolumeController.Stub { private final String TAG = VolumeDialogControllerImpl.TAG + ".VC"; @@ -1256,7 +1325,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks { private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); - private int mNextStream = DYNAMIC_STREAM_START_INDEX; + private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; private final boolean mVolumeAdjustmentForRemoteGroupSessions; public MediaSessionsCallbacks(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 6b02e1ada491..0770d8926389 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -1678,6 +1679,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, return true; } + // Always show the stream for audio sharing if it exists. + if (volumeDialogAudioSharingFix() + && row.ss != null + && mContext.getString(R.string.audio_sharing_description) + .equals(row.ss.remoteLabel)) { + return true; + } + if (row.defaultStream) { return activeRow.stream == STREAM_RING || activeRow.stream == STREAM_ALARM @@ -1880,10 +1889,25 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (!ss.dynamic) continue; mDynamic.put(stream, true); if (findRow(stream) == null) { - addRow(stream, - com.android.settingslib.R.drawable.ic_volume_remote, - com.android.settingslib.R.drawable.ic_volume_remote_mute, - true, false, true); + if (volumeDialogAudioSharingFix() + && mContext.getString(R.string.audio_sharing_description) + .equals(ss.remoteLabel)) { + addRow( + stream, + R.drawable.ic_volume_media, + R.drawable.ic_volume_media_mute, + true, + false, + true); + } else { + addRow( + stream, + com.android.settingslib.R.drawable.ic_volume_remote, + com.android.settingslib.R.drawable.ic_volume_remote_mute, + true, + false, + true); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt index 29040923a2cc..cf80263b4d14 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt @@ -16,20 +16,15 @@ package com.android.systemui.volume.dagger -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.volume.domain.interactor.AudioSharingInteractor import com.android.systemui.volume.domain.interactor.AudioSharingInteractorEmptyImpl +import dagger.Binds import dagger.Module -import dagger.Provides /** Dagger module for empty audio sharing impl for unnecessary volume overlay */ @Module interface AudioSharingEmptyImplModule { - companion object { - @Provides - @SysUISingleton - fun provideAudioSharingInteractor(): AudioSharingInteractor = - AudioSharingInteractorEmptyImpl() - } + @Binds + fun bindsAudioSharingInteractor(impl: AudioSharingInteractorEmptyImpl): AudioSharingInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt index 4d29788edf68..aba3015a6b7d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt @@ -77,7 +77,7 @@ constructor( } @SysUISingleton -class AudioSharingInteractorEmptyImpl : AudioSharingInteractor { +class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor { override val volume: Flow<Int?> = emptyFlow() override val volumeMin: Int = EMPTY_VOLUME override val volumeMax: Int = EMPTY_VOLUME diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index ec9b5cfbdeb2..14cd202b7ce6 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -20,8 +20,9 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_B 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.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; @@ -66,6 +67,7 @@ import com.android.wm.shell.onehanded.OneHandedEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.onehanded.OneHandedUiEventLogger; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.sysui.ShellInterface; @@ -249,7 +251,25 @@ public final class WMShell implements pip.showPictureInPictureMenu(); } }); + pip.registerPipTransitionCallback( + new PipTransitionController.PipTransitionCallback() { + @Override + public void onPipTransitionStarted(int direction, Rect pipBounds) { + mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, true) + .commitUpdate(mDisplayTracker.getDefaultDisplayId()); + } + + @Override + public void onPipTransitionFinished(int direction) { + mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, false) + .commitUpdate(mDisplayTracker.getDefaultDisplayId()); + } + @Override + public void onPipTransitionCanceled(int direction) { + // No op. + } + }, mSysUiMainExecutor); mSysUiState.addCallback(sysUiStateFlag -> { mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0; pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag); @@ -263,6 +283,11 @@ public final class WMShell implements public void onFinishedWakingUp() { splitScreen.onFinishedWakingUp(); } + + @Override + public void onStartedGoingToSleep() { + splitScreen.onStartedGoingToSleep(); + } }); mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java index 41a41164b2a5..038b81b34d77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java @@ -216,6 +216,16 @@ public class MagnificationTest extends SysuiTestCase { } @Test + public void onRestoreWindowSize_updateSettingsButtonStatusOnRestore() { + mMagnification.mWindowMagnifierCallback + .onWindowMagnifierBoundsRestored(TEST_DISPLAY, MagnificationSize.SMALL); + waitForIdleSync(); + + verify(mMagnificationSettingsController) + .updateSettingsButtonStatusOnRestore(MagnificationSize.SMALL); + } + + @Test public void onSetMagnifierSize_delegateToMagnifier() { final @MagnificationSize int index = MagnificationSize.SMALL; mMagnification.mMagnificationSettingsControllerCallback.onSetMagnifierSize( diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java index e27268292763..e01366a9e594 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java @@ -67,9 +67,8 @@ import android.graphics.Rect; import android.os.Handler; import android.os.RemoteException; import android.os.SystemClock; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.testing.TestableResources; @@ -130,14 +129,12 @@ import java.util.function.Supplier; @LargeTest @TestableLooper.RunWithLooper @RunWith(AndroidJUnit4.class) -@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) +@EnableFlags(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase { @Rule // NOTE: pass 'null' to allow this test advances time on the main thread. public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(/* test= */ null); - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; @Mock @@ -625,6 +622,7 @@ public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiT 0); } + @DisableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS) @Test public void onScreenSizeAndDensityChanged_enabled_restoreSavedMagnifierWindow() { int newSmallestScreenWidthDp = @@ -663,6 +661,50 @@ public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiT assertTrue(params.height == (windowFrameSize + 2 * mirrorSurfaceMargin)); } + @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS) + @Test + public void onScreenSizeAndDensityChanged_enabled_restoreSavedMagnifierIndexAndWindow() { + int newSmallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + int windowFrameSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + Size preferredWindowSize = new Size(windowFrameSize, windowFrameSize); + mSharedPreferences + .edit() + .putString(String.valueOf(newSmallestScreenWidthDp), + WindowMagnificationFrameSpec.serialize( + WindowMagnificationSettings.MagnificationSize.CUSTOM, + preferredWindowSize)) + .commit(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.updateWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + // Screen density and size change + mContext.getResources().getConfiguration().smallestScreenWidthDp = newSmallestScreenWidthDp; + final Rect testWindowBounds = new Rect( + mWindowManager.getCurrentWindowMetrics().getBounds()); + testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, + testWindowBounds.right + 100, testWindowBounds.bottom + 100); + mWindowManager.setWindowBounds(testWindowBounds); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + + // wait for rect update + waitForIdleSync(); + verify(mWindowMagnifierCallback).onWindowMagnifierBoundsRestored( + eq(mContext.getDisplayId()), + eq(WindowMagnificationSettings.MagnificationSize.CUSTOM)); + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // The width and height of the view include the magnification frame and the margins. + assertTrue(params.width == (windowFrameSize + 2 * mirrorSurfaceMargin)); + assertTrue(params.height == (windowFrameSize + 2 * mirrorSurfaceMargin)); + } + @Test public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() { mInstrumentation.runOnMainSync(() -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java index ad9053a7edda..944066fa4954 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility; +import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; @@ -23,12 +25,15 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.util.Size; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.util.FakeSharedPreferences; @@ -54,19 +59,59 @@ public class WindowMagnificationFrameSizePrefsTest extends SysuiTestCase { mWindowMagnificationFrameSizePrefs = new WindowMagnificationFrameSizePrefs(mContext); } + @DisableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS) @Test public void saveSizeForCurrentDensity_getExpectedSize() { Size testSize = new Size(500, 500); - mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(testSize); + mWindowMagnificationFrameSizePrefs + .saveIndexAndSizeForCurrentDensity(MagnificationSize.CUSTOM, testSize); assertThat(mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity()) .isEqualTo(testSize); } + @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS) + @Test + public void saveSizeForCurrentDensity_validPreference_getExpectedSize() { + int testIndex = MagnificationSize.MEDIUM; + Size testSize = new Size(500, 500); + mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(testIndex, testSize); + + assertThat(mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity()) + .isEqualTo(testSize); + } + + @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS) + @Test + public void saveSizeForCurrentDensity_validPreference_getExpectedIndex() { + int testIndex = MagnificationSize.MEDIUM; + Size testSize = new Size(500, 500); + mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(testIndex, testSize); + + assertThat(mWindowMagnificationFrameSizePrefs.getIndexForCurrentDensity()) + .isEqualTo(testIndex); + } + + @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS) + @Test + public void saveSizeForCurrentDensity_invalidPreference_getDefaultIndex() { + mSharedPreferences + .edit() + .putString( + String.valueOf( + mContext.getResources().getConfiguration().smallestScreenWidthDp), + "100x200") + .commit(); + + assertThat(mWindowMagnificationFrameSizePrefs.getIndexForCurrentDensity()) + .isEqualTo(MagnificationSize.DEFAULT); + } + @Test public void saveSizeForCurrentDensity_containsPreferenceForCurrentDensity() { + int testIndex = MagnificationSize.MEDIUM; Size testSize = new Size(500, 500); - mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(testSize); + mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(testIndex, testSize); assertThat(mWindowMagnificationFrameSizePrefs.isPreferenceSavedForCurrentDensity()) .isTrue(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt new file mode 100644 index 000000000000..791a26ed761d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.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.systemui.accessibility + +import android.testing.AndroidTestingRunner +import android.util.Size +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class WindowMagnificationFrameSpecTest : SysuiTestCase() { + + @Test + fun deserializeSpec_validSpec_expectedIndex() { + val targetIndex = MagnificationSize.LARGE + val targetSize = Size(100, 200) + val targetPreference = WindowMagnificationFrameSpec.serialize(targetIndex, targetSize) + + assertThat(WindowMagnificationFrameSpec.deserialize(targetPreference).index) + .isEqualTo(targetIndex) + } + + @Test + fun deserializeSpec_validSpec_expectedSize() { + val targetIndex = MagnificationSize.LARGE + val targetSize = Size(100, 200) + val targetPreference = WindowMagnificationFrameSpec.serialize(targetIndex, targetSize) + + assertThat(WindowMagnificationFrameSpec.deserialize(targetPreference).size) + .isEqualTo(targetSize) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java index 639b53bfb95b..5600b87280ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java @@ -710,6 +710,35 @@ public class TouchMonitorTest extends SysuiTestCase { environment.verifyLifecycleObserversUnregistered(); } + @Test + public void testLastSessionPop_createsNewInputSession() { + final TouchHandler touchHandler = createTouchHandler(); + + final TouchHandler.TouchSession.Callback callback = + Mockito.mock(TouchHandler.TouchSession.Callback.class); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new)), mKosmos); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final TouchHandler.TouchSession session = captureSession(touchHandler); + session.registerCallback(callback); + + // Clear invocations on input session and factory. + clearInvocations(environment.mInputFactory); + clearInvocations(environment.mInputSession); + + // Pop only active touch session. + session.pop(); + environment.executeAll(); + + // Verify that input session disposed and new session requested from factory. + verify(environment.mInputSession).dispose(); + verify(environment.mInputFactory).create(any(), any(), any(), anyBoolean()); + } + private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) { final GestureDetector.OnGestureListener gestureListener = Mockito.mock( GestureDetector.OnGestureListener.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java index d940fc938c4b..7a4bbfe9a580 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java @@ -36,7 +36,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.policy.BatteryController; @@ -77,9 +76,6 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { when(mBatteryMeterView.getContext()).thenReturn(mContext); when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources()); - - mContext.getOrCreateTestableResources().addOverride( - R.bool.flag_battery_shield_icon, false); } @Test @@ -136,26 +132,6 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { verify(mTunerService, never()).addTunable(any(), any()); } - @Test - public void shieldFlagDisabled_viewNotified() { - mContext.getOrCreateTestableResources().addOverride( - R.bool.flag_battery_shield_icon, false); - - initController(); - - verify(mBatteryMeterView).setDisplayShieldEnabled(false); - } - - @Test - public void shieldFlagEnabled_viewNotified() { - mContext.getOrCreateTestableResources().addOverride( - R.bool.flag_battery_shield_icon, true); - - initController(); - - verify(mBatteryMeterView).setDisplayShieldEnabled(true); - } - private void initController() { mController = new BatteryMeterViewController( mBatteryMeterView, diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt index 2bd0976f30dc..2aa33a176ac9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt @@ -22,9 +22,9 @@ import android.widget.ImageView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.flags.Flags.FLAG_NEW_STATUS_BAR_ICONS -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -67,9 +67,8 @@ class BatteryMeterViewTest : SysuiTestCase() { fun contentDescription_unknown() { mBatteryMeterView.onBatteryUnknownStateChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_unknown) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_unknown)) } @Test @@ -80,11 +79,10 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString( - R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE - ) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( + context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE) + ) } @Test @@ -96,13 +94,14 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( context.getString( - R.string.accessibility_battery_level_charging_paused_with_estimate, - 17, - ESTIMATE, + R.string.accessibility_battery_level_charging_paused_with_estimate, + 17, + ESTIMATE, ) - ) + ) } @Test @@ -110,27 +109,24 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.onBatteryLevelChanged(90, false) mBatteryMeterView.onIsBatteryDefenderChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging_paused, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90)) } @Test fun contentDescription_charging() { mBatteryMeterView.onBatteryLevelChanged(45, true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging, 45) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 45)) } @Test fun contentDescription_notCharging() { mBatteryMeterView.onBatteryLevelChanged(45, false) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 45) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 45)) } @Test @@ -138,9 +134,8 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.onBatteryLevelChanged(45, true) mBatteryMeterView.onIsIncompatibleChargingChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 45) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 45)) } @Test @@ -152,19 +147,17 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString( - R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE - ) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( + context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE) + ) // Update the show mode from estimate to percent mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON) assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%") - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 15) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 15)) } @Test @@ -176,19 +169,17 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString( - R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( + context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE) ) - ) // Update the show mode from estimate to percent mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON) assertThat(mBatteryMeterView.batteryPercentView).isNull() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 15) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 15)) assertThat(mBatteryMeterView.unifiedBatteryState.showPercent).isTrue() } @@ -225,49 +216,47 @@ class BatteryMeterViewTest : SysuiTestCase() { // BatteryDefender mBatteryMeterView.onBatteryLevelChanged(90, false) mBatteryMeterView.onIsBatteryDefenderChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging_paused, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90)) // BatteryDefender & estimate mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) mBatteryMeterView.setBatteryEstimateFetcher(Fetcher()) mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( context.getString( - R.string.accessibility_battery_level_charging_paused_with_estimate, - 90, - ESTIMATE, + R.string.accessibility_battery_level_charging_paused_with_estimate, + 90, + ESTIMATE, ) - ) + ) // Just estimate mBatteryMeterView.onIsBatteryDefenderChanged(false) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( context.getString( - R.string.accessibility_battery_level_with_estimate, - 90, - ESTIMATE, + R.string.accessibility_battery_level_with_estimate, + 90, + ESTIMATE, ) - ) + ) // Just percent mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 90)) // Charging mBatteryMeterView.onBatteryLevelChanged(90, true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 90)) } @Test @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOff() { - mBatteryMeterView.setDisplayShieldEnabled(true) val drawable = getBatteryDrawable() mBatteryMeterView.onIsBatteryDefenderChanged(true) @@ -278,8 +267,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOn() { - mBatteryMeterView.setDisplayShieldEnabled(true) - mBatteryMeterView.onIsBatteryDefenderChanged(true) assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNotNull() @@ -288,7 +275,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOff() { - mBatteryMeterView.setDisplayShieldEnabled(true) val drawable = getBatteryDrawable() // Start as true @@ -303,8 +289,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOn() { - mBatteryMeterView.setDisplayShieldEnabled(true) - // Start as true mBatteryMeterView.onIsBatteryDefenderChanged(true) @@ -316,27 +300,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) - fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOff() { - mBatteryMeterView.setDisplayShieldEnabled(false) - val drawable = getBatteryDrawable() - - mBatteryMeterView.onIsBatteryDefenderChanged(true) - - assertThat(drawable.displayShield).isFalse() - } - - @Test - @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS) - fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOn() { - mBatteryMeterView.setDisplayShieldEnabled(false) - - mBatteryMeterView.onIsBatteryDefenderChanged(true) - - assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNull() - } - - @Test - @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isIncompatibleChargingChanged_true_drawableGetsChargingFalse_flagOff() { mBatteryMeterView.onBatteryLevelChanged(45, true) val drawable = getBatteryDrawable() @@ -381,8 +344,8 @@ class BatteryMeterViewTest : SysuiTestCase() { } private fun getBatteryDrawable(): AccessorizedBatteryDrawable { - return (mBatteryMeterView.getChildAt(0) as ImageView) - .drawable as AccessorizedBatteryDrawable + return (mBatteryMeterView.getChildAt(0) as ImageView).drawable + as AccessorizedBatteryDrawable } private class Fetcher : BatteryEstimateFetcher { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 677d1fdaa01d..6dcea144f2a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -449,150 +449,214 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - fun shows_authenticated_no_errors_no_confirmation_required() = runGenericTest { - if (!testCase.confirmationRequested) { - val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - val iconOverlayAsset by - collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) - val iconContentDescriptionId by - collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) - val shouldAnimateIconView by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) - val shouldAnimateIconOverlay by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) - verifyIconSize() + fun shows_error_to_unlock_or_success() { + // Face-only auth does not use error -> unlock or error -> success assets + if (testCase.isFingerprintOnly || testCase.isCoex) { + runGenericTest { + // Distinct asset for error -> success only applicable for fingerprint-only / + // explicit co-ex auth + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + + var forceExplicitFlow = + testCase.isCoex && testCase.confirmationRequested || + testCase.authenticatedByFingerprint + if (forceExplicitFlow) { + kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true) + } + verifyIconSize(forceExplicitFlow) - kosmos.promptViewModel.showAuthenticated( - modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY - ) + kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true) + kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true) - if (testCase.isFingerprintOnly) { - // Fingerprint icon asset assertions - if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { - assertThat(iconAsset).isEqualTo(getSfpsBaseIconAsset()) - assertThat(iconOverlayAsset) - .isEqualTo(R.raw.biometricprompt_symbol_fingerprint_to_success_landscape) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(true) - } else { - assertThat(iconAsset) - .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie) + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + // TODO(b/350121748): SFPS test cases to be added after SFPS assets update + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + // Non-SFPS (UDFPS / rear-FPS) test cases + // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint + if (testCase.authenticatedByFingerprint) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_success_lottie) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + } else { // co-ex, authenticated by face + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + assertThat(shouldAnimateIconView).isEqualTo(true) + + // Confirm authentication + kosmos.promptViewModel.confirmAuthenticated() + + assertThat(iconAsset) + .isEqualTo( + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + ) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + } + } + } + } + } + + @Test + fun shows_authenticated_no_errors_no_confirmation_required() { + if (!testCase.confirmationRequested) { + runGenericTest { + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconOverlayAsset by + collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val shouldAnimateIconOverlay by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) + verifyIconSize() + + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + if (testCase.isFingerprintOnly) { + // Fingerprint icon asset assertions + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset).isEqualTo(getSfpsBaseIconAsset()) + assertThat(iconOverlayAsset) + .isEqualTo( + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + ) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } else if (testCase.isFaceOnly || testCase.isCoex) { + // Face icon asset assertions + // If co-ex, use implicit flow (explicit flow always requires confirmation) + assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) assertThat(iconOverlayAsset).isEqualTo(-1) assertThat(iconContentDescriptionId) - .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) assertThat(shouldAnimateIconOverlay).isEqualTo(false) } - } else if (testCase.isFaceOnly || testCase.isCoex) { - // Face icon asset assertions - // If co-ex, use implicit flow (explicit flow always requires confirmation) - assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) - assertThat(iconOverlayAsset).isEqualTo(-1) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(false) } } } @Test - fun shows_pending_confirmation() = runGenericTest { - if ( - (testCase.isFaceOnly || testCase.isCoex) && - testCase.authenticatedByFace && - testCase.confirmationRequested - ) { - val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - val iconOverlayAsset by - collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) - val iconContentDescriptionId by - collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) - val shouldAnimateIconView by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) - val shouldAnimateIconOverlay by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) - - val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested - verifyIconSize(forceExplicitFlow = forceExplicitFlow) - - kosmos.promptViewModel.showAuthenticated( - modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY - ) + fun shows_pending_confirmation() { + if (testCase.authenticatedByFace && testCase.confirmationRequested) { + runGenericTest { + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconOverlayAsset by + collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val shouldAnimateIconOverlay by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) + + val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested + verifyIconSize(forceExplicitFlow = forceExplicitFlow) + + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) - if (testCase.isFaceOnly) { - assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark) - assertThat(iconOverlayAsset).isEqualTo(-1) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(false) - } else if (testCase.isCoex) { // explicit flow, confirmation requested - // TODO: Update when SFPS co-ex is implemented - if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { - assertThat(iconAsset) - .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie) + if (testCase.isFaceOnly) { + assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark) assertThat(iconOverlayAsset).isEqualTo(-1) assertThat(iconContentDescriptionId) - .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } else if (testCase.isCoex) { // explicit flow, confirmation requested + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } } } } } @Test - fun shows_authenticated_explicitly_confirmed_iconUpdate() = runGenericTest { - if ( - (testCase.isFaceOnly || testCase.isCoex) && - testCase.authenticatedByFace && - testCase.confirmationRequested - ) { - val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - val iconOverlayAsset by - collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) - val iconContentDescriptionId by - collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) - val shouldAnimateIconView by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) - val shouldAnimateIconOverlay by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) - val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested - verifyIconSize(forceExplicitFlow = forceExplicitFlow) - - kosmos.promptViewModel.showAuthenticated( - modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY - ) - - kosmos.promptViewModel.confirmAuthenticated() + fun shows_authenticated_explicitly_confirmed() { + if (testCase.authenticatedByFace && testCase.confirmationRequested) { + runGenericTest { + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconOverlayAsset by + collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val shouldAnimateIconOverlay by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) + val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested + verifyIconSize(forceExplicitFlow = forceExplicitFlow) + + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) - if (testCase.isFaceOnly) { - assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) - assertThat(iconOverlayAsset).isEqualTo(-1) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(false) - } + kosmos.promptViewModel.confirmAuthenticated() - // explicit flow because confirmation requested - if (testCase.isCoex) { - // TODO: Update when SFPS co-ex is implemented - if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { - assertThat(iconAsset) - .isEqualTo(R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie) + if (testCase.isFaceOnly) { + assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) assertThat(iconOverlayAsset).isEqualTo(-1) assertThat(iconContentDescriptionId) - .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) assertThat(shouldAnimateIconView).isEqualTo(true) assertThat(shouldAnimateIconOverlay).isEqualTo(false) } + + // explicit flow because confirmation requested + if (testCase.isCoex) { + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo( + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + ) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } } } } @@ -700,58 +764,68 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - fun sfpsIconUpdates_onFoldConfigurationChanged() = runGenericTest { + fun sfpsIconUpdates_onFoldConfigurationChanged() { if ( testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON && !testCase.isInRearDisplayMode ) { - val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + runGenericTest { + val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - kosmos.promptViewModel.iconViewModel.onConfigurationChanged(getFoldedConfiguration()) - val foldedIcon = currentIcon + kosmos.promptViewModel.iconViewModel.onConfigurationChanged( + getFoldedConfiguration() + ) + val foldedIcon = currentIcon - kosmos.promptViewModel.iconViewModel.onConfigurationChanged(getUnfoldedConfiguration()) - val unfoldedIcon = currentIcon + kosmos.promptViewModel.iconViewModel.onConfigurationChanged( + getUnfoldedConfiguration() + ) + val unfoldedIcon = currentIcon - assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + } } } @Test - fun sfpsIconUpdates_onRotation() = runGenericTest { + fun sfpsIconUpdates_onRotation() { if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { - val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + runGenericTest { + val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) - val iconRotation0 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) + val iconRotation0 = currentIcon - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) - val iconRotation90 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + val iconRotation90 = currentIcon - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) - val iconRotation180 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) + val iconRotation180 = currentIcon - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) - val iconRotation270 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + val iconRotation270 = currentIcon - assertThat(iconRotation0).isEqualTo(iconRotation180) - assertThat(iconRotation0).isNotEqualTo(iconRotation90) - assertThat(iconRotation0).isNotEqualTo(iconRotation270) + assertThat(iconRotation0).isEqualTo(iconRotation180) + assertThat(iconRotation0).isNotEqualTo(iconRotation90) + assertThat(iconRotation0).isNotEqualTo(iconRotation270) + } } } @Test - fun sfpsIconUpdates_onRearDisplayMode() = runGenericTest { + fun sfpsIconUpdates_onRearDisplayMode() { if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { - val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + runGenericTest { + val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - kosmos.displayStateRepository.setIsInRearDisplayMode(false) - val iconNotRearDisplayMode = currentIcon + kosmos.displayStateRepository.setIsInRearDisplayMode(false) + val iconNotRearDisplayMode = currentIcon - kosmos.displayStateRepository.setIsInRearDisplayMode(true) - val iconRearDisplayMode = currentIcon + kosmos.displayStateRepository.setIsInRearDisplayMode(true) + val iconRearDisplayMode = currentIcon - assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode) + assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSourceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSourceTest.kt new file mode 100644 index 000000000000..4d112e93013a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSourceTest.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.systemui.keyboard.shortcut.data.source + +import android.view.KeyboardShortcutGroup +import android.view.WindowManager +import android.view.mockWindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CurrentAppShortcutsSourceTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val mockWindowManager = kosmos.mockWindowManager + private val source = CurrentAppShortcutsSource(mockWindowManager) + + private var shortcutGroups: List<KeyboardShortcutGroup>? = null + + @Before + fun setUp() { + whenever(mockWindowManager.requestAppKeyboardShortcuts(any(), any())).thenAnswer { + val receiver = it.arguments[0] as WindowManager.KeyboardShortcutsReceiver + receiver.onKeyboardShortcutsReceived(shortcutGroups) + } + } + + @Test + fun shortcutGroups_wmReturnsNullList_returnsEmptyList() = + testScope.runTest { + shortcutGroups = null + + val groups = source.shortcutGroups(TEST_DEVICE_ID) + + assertThat(groups).isEmpty() + } + + @Test + fun shortcutGroups_wmReturnsEmptyList_returnsEmptyList() = + testScope.runTest { + shortcutGroups = emptyList() + + val groups = source.shortcutGroups(TEST_DEVICE_ID) + + assertThat(groups).isEmpty() + } + + @Test + fun shortcutGroups_wmReturnsGroups_returnsWmGroups() = + testScope.runTest { + shortcutGroups = + listOf( + KeyboardShortcutGroup("wm ime group 1"), + KeyboardShortcutGroup("wm ime group 2"), + KeyboardShortcutGroup("wm ime group 3"), + ) + + val groups = source.shortcutGroups(TEST_DEVICE_ID) + + assertThat(groups).hasSize(3) + } + + companion object { + private const val TEST_DEVICE_ID = 9876 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSourceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSourceTest.kt new file mode 100644 index 000000000000..715d907b7372 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSourceTest.kt @@ -0,0 +1,94 @@ +/* + * 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.systemui.keyboard.shortcut.data.source + +import android.content.res.mainResources +import android.view.KeyboardShortcutGroup +import android.view.WindowManager.KeyboardShortcutsReceiver +import android.view.mockWindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +class InputShortcutsSourceTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val mockWindowManager = kosmos.mockWindowManager + private val source = InputShortcutsSource(kosmos.mainResources, mockWindowManager) + + private var wmImeShortcutGroups: List<KeyboardShortcutGroup>? = null + + @Before + fun setUp() { + whenever(mockWindowManager.requestImeKeyboardShortcuts(any(), any())).thenAnswer { + val receiver = it.arguments[0] as KeyboardShortcutsReceiver + receiver.onKeyboardShortcutsReceived(wmImeShortcutGroups) + } + } + + @Test + fun shortcutGroups_wmReturnsNullList_returnsSingleGroup() = + testScope.runTest { + wmImeShortcutGroups = null + + val groups = source.shortcutGroups(TEST_DEVICE_ID) + + assertThat(groups).hasSize(1) + } + + @Test + fun shortcutGroups_wmReturnsEmptyList_returnsSingleGroup() = + testScope.runTest { + wmImeShortcutGroups = emptyList() + + val groups = source.shortcutGroups(TEST_DEVICE_ID) + + assertThat(groups).hasSize(1) + } + + @Test + fun shortcutGroups_wmReturnsGroups_returnsWmGroupsPlusOne() = + testScope.runTest { + wmImeShortcutGroups = + listOf( + KeyboardShortcutGroup("wm ime group 1"), + KeyboardShortcutGroup("wm ime group 2"), + KeyboardShortcutGroup("wm ime group 3"), + ) + + val groups = source.shortcutGroups(TEST_DEVICE_ID) + + assertThat(groups).hasSize(4) + } + + companion object { + private const val TEST_DEVICE_ID = 1234 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 4f4aac454912..3b96be48b2cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -152,7 +152,6 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC val featureFlags = FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 9fb1aa795c3c..e89abf6fc5a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -195,7 +195,6 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { val featureFlags = FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt index d183c7370713..7dd802878674 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt @@ -47,13 +47,7 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { private lateinit var dialog: AlertDialog private val flags = mock<FeatureFlagsClassic>() - private val onStartRecordingClicked = mock<Runnable>() - private val mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>() - - private val mediaProjectionConfig: MediaProjectionConfig = - MediaProjectionConfig.createConfigForDefaultDisplay() - private val appName: String = "testApp" - private val hostUid: Int = 12345 + private val appName = "Test App" private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen @@ -73,16 +67,33 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { } @Test - fun showDialog_forceShowPartialScreenShareFalse() { - // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and - // overrideDisableSingleAppOption = false - val overrideDisableSingleAppOption = false - setUpAndShowDialog(overrideDisableSingleAppOption) + fun showDefaultDialog() { + setUpAndShowDialog() val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) val secondOptionText = spinner.adapter .getDropDownView(1, null, spinner) + .findViewById<TextView>(android.R.id.text1) + ?.text + + // check that the first option is single app and enabled + assertEquals(context.getString(resIdSingleApp), spinner.selectedItem) + + // check that the second option is full screen and enabled + assertEquals(context.getString(resIdFullScreen), secondOptionText) + } + + @Test + fun showDialog_disableSingleApp() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay() + ) + + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + val secondOptionWarningText = + spinner.adapter + .getDropDownView(1, null, spinner) .findViewById<TextView>(android.R.id.text2) ?.text @@ -90,15 +101,15 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) // check that the second option is single app and disabled - assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionText) + assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText) } @Test - fun showDialog_forceShowPartialScreenShareTrue() { - // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and - // overrideDisableSingleAppOption = true - val overrideDisableSingleAppOption = true - setUpAndShowDialog(overrideDisableSingleAppOption) + fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(), + overrideDisableSingleAppOption = true + ) val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) val secondOptionText = @@ -114,17 +125,43 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { assertEquals(context.getString(resIdFullScreen), secondOptionText) } - private fun setUpAndShowDialog(overrideDisableSingleAppOption: Boolean) { + @Test + fun showDialog_disableSingleApp_hasCastingCapabilities() { + setUpAndShowDialog( + mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(), + hasCastingCapabilities = true + ) + + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + val secondOptionWarningText = + spinner.adapter + .getDropDownView(1, null, spinner) + .findViewById<TextView>(android.R.id.text2) + ?.text + + // check that the first option is full screen and enabled + assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) + + // check that the second option is single app and disabled + assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText) + } + + private fun setUpAndShowDialog( + mediaProjectionConfig: MediaProjectionConfig? = null, + overrideDisableSingleAppOption: Boolean = false, + hasCastingCapabilities: Boolean = false, + ) { val delegate = MediaProjectionPermissionDialogDelegate( context, mediaProjectionConfig, - {}, - onStartRecordingClicked, + onStartRecordingClicked = {}, + onCancelClicked = {}, + hasCastingCapabilities, appName, overrideDisableSingleAppOption, - hostUid, - mediaProjectionMetricsLogger + hostUid = 12345, + mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>() ) dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index e46416c1b321..ebab04989590 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -75,6 +75,7 @@ import com.android.systemui.qs.QsEventLoggerFake; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.statusbar.StatusBarState; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -146,6 +147,12 @@ public class QSTileImplTest extends SysuiTestCase { mTile.setTileSpec(SPEC); } + @After + public void destroyTile() { + mTile.destroy(); + mTestableLooper.processAllMessages(); + } + @Test public void testClick_Metrics() { mTile.click(null /* expandable */); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt index 25dd9fedba7c..24e8b1886438 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt @@ -27,7 +27,6 @@ import android.os.UserHandle import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.screenshot.ScreenshotController.SaveImageInBackgroundData import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -44,7 +43,7 @@ class SaveImageInBackgroundTaskTest : SysuiTestCase() { private val imageExporter = mock<ImageExporter>() private val smartActions = mock<ScreenshotSmartActions>() private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>() - private val saveImageData = SaveImageInBackgroundData() + private val saveImageData = SaveImageInBackgroundTask.SaveImageInBackgroundData() private val testScreenshotId: String = "testScreenshotId" private val testBitmap = mock<Bitmap>() private val testUser = UserHandle.getUserHandleForUid(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt index 3b0e194ce223..bab9bbbfde4f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt @@ -16,13 +16,13 @@ package com.android.systemui.screenshot.policy import android.content.ComponentName -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.graphics.Insets import android.graphics.Rect import android.os.UserHandle import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.ScreenshotData import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES @@ -40,20 +40,23 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PolicyRequestProcessorTest { - val imageCapture = object : ImageCapture { - override fun captureDisplay(displayId: Int, crop: Rect?) = null - override suspend fun captureTask(taskId: Int) = null - } + val imageCapture = + object : ImageCapture { + override fun captureDisplay(displayId: Int, crop: Rect?) = null + + override suspend fun captureTask(taskId: Int) = null + } /** Tests behavior when no policies are applied */ @Test fun testProcess_defaultOwner_whenNoPolicyApplied() { val fullScreenWork = DisplayContentRepository { - singleFullScreen(TaskSpec(taskId = 1001, name = FILES, userId = WORK)) + singleFullScreen(TaskSpec(taskId = TASK_ID, name = FILES, userId = WORK)) } val request = - ScreenshotData(TAKE_SCREENSHOT_FULLSCREEN, + ScreenshotData( + TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD, null, topComponent = null, @@ -61,24 +64,34 @@ class PolicyRequestProcessorTest { taskId = -1, insets = Insets.NONE, bitmap = null, - displayId = DEFAULT_DISPLAY) + displayId = DEFAULT_DISPLAY + ) /* Create a policy request processor with no capture policies */ val requestProcessor = - PolicyRequestProcessor(Dispatchers.Unconfined, + PolicyRequestProcessor( + Dispatchers.Unconfined, imageCapture, policies = emptyList(), defaultOwner = UserHandle.of(PERSONAL), defaultComponent = ComponentName("default", "Component"), - displayTasks = fullScreenWork) + displayTasks = fullScreenWork + ) val result = runBlocking { requestProcessor.process(request) } - assertWithMessage( - "With no policy, the screenshot should be assigned to the default user" - ).that(result.userHandle).isEqualTo(UserHandle.of(PERSONAL)) + assertWithMessage("With no policy, the screenshot should be assigned to the default user") + .that(result.userHandle) + .isEqualTo(UserHandle.of(PERSONAL)) + + assertWithMessage("The topComponent of the screenshot") + .that(result.topComponent) + .isEqualTo(ComponentName.unflattenFromString(FILES)) + + assertWithMessage("Task ID").that(result.taskId).isEqualTo(TASK_ID) + } - assertWithMessage("The topComponent of the screenshot").that(result.topComponent) - .isEqualTo(ComponentName.unflattenFromString(FILES)) + companion object { + const val TASK_ID = 1001 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 169511f827ef..967df39c9269 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -48,7 +48,12 @@ import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -67,13 +72,13 @@ import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyFloat -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @RunWith(AndroidTestingRunner::class) @@ -87,11 +92,11 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { testDispatcher = UnconfinedTestDispatcher() } - @Mock private lateinit var communalViewModel: CommunalViewModel - @Mock private lateinit var powerManager: PowerManager - @Mock private lateinit var touchMonitor: TouchMonitor - @Mock private lateinit var communalColors: CommunalColors - @Mock private lateinit var communalContent: CommunalContent + private var communalViewModel = mock<CommunalViewModel>() + private var powerManager = mock<PowerManager>() + private var touchMonitor = mock<TouchMonitor>() + private var communalColors = mock<CommunalColors>() + private var communalContent = mock<CommunalContent>() private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory private lateinit var parentView: FrameLayout @@ -103,8 +108,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Before fun setUp() { - MockitoAnnotations.initMocks(this) - communalRepository = kosmos.fakeCommunalSceneRepository ambientTouchComponentFactory = @@ -124,6 +127,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { communalInteractor, communalViewModel, keyguardInteractor, + kosmos.keyguardTransitionInteractor, shadeInteractor, powerManager, communalColors, @@ -167,6 +171,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { communalInteractor, communalViewModel, keyguardInteractor, + kosmos.keyguardTransitionInteractor, shadeInteractor, powerManager, communalColors, @@ -192,6 +197,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { communalInteractor, communalViewModel, keyguardInteractor, + kosmos.keyguardTransitionInteractor, shadeInteractor, powerManager, communalColors, @@ -212,6 +218,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { communalInteractor, communalViewModel, keyguardInteractor, + kosmos.keyguardTransitionInteractor, shadeInteractor, powerManager, communalColors, @@ -235,12 +242,15 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - fun lifecycle_resumedAfterCommunalShows() { - // Communal is open. - goToScene(CommunalScenes.Communal) + fun lifecycle_resumedAfterCommunalShows() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) - assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) - } + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + } @Test fun lifecycle_startedAfterCommunalCloses() = @@ -289,6 +299,43 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun lifecycle_startedWhenEditActivityShowing() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Edit activity is showing. + communalInteractor.setEditActivityShowing(true) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + } + } + + @Test + fun lifecycle_startedWhenEditModeTransitionStarted() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Leaving edit mode to return to the hub. + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.GLANCEABLE_HUB, + value = 1.0f, + transitionState = TransitionState.RUNNING + ) + ) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + } + } + + @Test fun lifecycle_createdAfterDisposeView() { // Container view disposed. underTest.disposeView() @@ -312,6 +359,59 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun lifecycle_doesNotResumeOnUserInteractivityOnceExpanded() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Shade shows up. + shadeTestUtil.setShadeExpansion(1.0f) + testableLooper.processAllMessages() + underTest.onTouchEvent(DOWN_EVENT) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Shade starts collapsing. + shadeTestUtil.setShadeExpansion(.5f) + testableLooper.processAllMessages() + underTest.onTouchEvent(DOWN_EVENT) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Shade fully collpase, and then expand should with touch interaction should now + // be resumed. + shadeTestUtil.setShadeExpansion(0f) + testableLooper.processAllMessages() + shadeTestUtil.setShadeExpansion(.5f) + testableLooper.processAllMessages() + underTest.onTouchEvent(DOWN_EVENT) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + } + + @Test + fun touchHandling_moveEventProcessedAfterCancel() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Shade shows up. + shadeTestUtil.setQsExpansion(0.5f) + testableLooper.processAllMessages() + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse() + assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue() + } + } + + @Test fun editMode_communalAvailable() = with(kosmos) { testScope.runTest { @@ -433,10 +533,10 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { testScope.runTest { // Communal is closed. goToScene(CommunalScenes.Blank) - `when`( + whenever( notificationStackScrollLayoutController.isBelowLastNotification( - anyFloat(), - anyFloat() + any(), + any() ) ) .thenReturn(false) @@ -444,6 +544,62 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @Test + fun onTouchEvent_hubOpen_touchesDispatched() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Touch event is sent to the container view. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + verify(containerView).onTouchEvent(any()) + } + } + + @Test + fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Transitioning to or from edit mode. + communalInteractor.setEditActivityShowing(true) + testableLooper.processAllMessages() + + // onTouchEvent returns true to consume the touch, but it is not sent to the + // container view. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + verify(containerView, never()).onTouchEvent(any()) + } + } + + @Test + fun onTouchEvent_editModeTransitionStarted_touchesConsumedButNotDispatched() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Leaving edit mode to return to the hub. + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.GLANCEABLE_HUB, + value = 1.0f, + transitionState = TransitionState.RUNNING + ) + ) + testableLooper.processAllMessages() + + // onTouchEvent returns true to consume the touch, but it is not sent to the + // container view. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + verify(containerView, never()).onTouchEvent(any()) + } + } + private fun initAndAttachContainerView() { val mockInsets = mock<WindowInsets> { @@ -462,8 +618,21 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { testableLooper.processAllMessages() } - private fun goToScene(scene: SceneKey) { + private suspend fun goToScene(scene: SceneKey) { communalRepository.changeScene(scene) + if (scene == CommunalScenes.Communal) { + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + kosmos.testScope + ) + } else { + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + kosmos.testScope + ) + } testableLooper.processAllMessages() } @@ -488,5 +657,35 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { CONTAINER_HEIGHT.toFloat() / 2, 0 ) + + private val CANCEL_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_CANCEL, + CONTAINER_WIDTH.toFloat() / 2, + CONTAINER_HEIGHT.toFloat() / 2, + 0 + ) + + private val MOVE_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_MOVE, + CONTAINER_WIDTH.toFloat() / 2, + CONTAINER_HEIGHT.toFloat() / 2, + 0 + ) + + private val UP_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_UP, + CONTAINER_WIDTH.toFloat() / 2, + CONTAINER_HEIGHT.toFloat() / 2, + 0 + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 8a6b68f26f66..a5c4bcd46a1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -675,8 +675,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mMainHandler = new Handler(Looper.getMainLooper()); + LongPressHandlingView longPressHandlingView = mock(LongPressHandlingView.class); when(mView.requireViewById(R.id.keyguard_long_press)) - .thenReturn(mock(LongPressHandlingView.class)); + .thenReturn(longPressHandlingView); + + Resources longPressHandlingViewRes = mock(Resources.class); + when(longPressHandlingView.getResources()).thenReturn(longPressHandlingViewRes); + when(longPressHandlingViewRes.getString(anyInt())).thenReturn(""); + mHeadsUpNotificationInteractor = new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt index 52af907c7b7d..64eadb7db1e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt @@ -36,7 +36,6 @@ import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt index acb005f6e72f..0407fc14d35a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt @@ -42,7 +42,7 @@ import org.mockito.quality.Strictness @RunWith(AndroidJUnit4::class) @SmallTest // this class has no testable logic with either of these flags enabled -@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME) +@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME) class NotificationSectionsFeatureManagerTest : SysuiTestCase() { lateinit var manager: NotificationSectionsFeatureManager private val proxyFake = DeviceConfigProxyFake() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index a925ccfe174b..b7995953b327 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -91,6 +91,7 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -1196,6 +1197,26 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @EnableFlags(NotificationThrottleHun.FLAG_NAME) + public void testGenerateHeadsUpAnimation_isSeenInShade_noAnimation() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + // Entry was seen in shade + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isSeenInShade()).thenReturn(true); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + when(row.getEntry()).thenReturn(entry); + + // WHEN we generate an add event + mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + + // THEN nothing happens + assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse(); + } + + @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) public void testOnChildAnimationsFinished_resetsheadsUpAnimatingAway() { // GIVEN NSSL is ready for HUN animations diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index cc2ef53c6cdb..12cfdcfa8df5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInterac import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.batteryController diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt index f40282f0da69..a8271fec0963 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt @@ -21,10 +21,9 @@ import android.view.InputDevice.SOURCE_MOUSE import android.view.MotionEvent import android.view.MotionEvent.CLASSIFICATION_NONE import android.view.MotionEvent.TOOL_TYPE_FINGER -import java.lang.reflect.Method -import org.mockito.kotlin.doNothing +import org.mockito.AdditionalAnswers +import org.mockito.Mockito.mock import org.mockito.kotlin.doReturn -import org.mockito.kotlin.spy import org.mockito.kotlin.whenever fun motionEvent( @@ -40,31 +39,18 @@ fun motionEvent( val event = MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0) event.source = source - val spy = - spy<MotionEvent>(event) { - on { getToolType(0) } doReturn toolType - on { getPointerCount() } doReturn pointerCount - axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value } - on { getClassification() } doReturn classification - } - ensureFinalizeIsNotCalledTwice(spy) - return spy -} - -private fun ensureFinalizeIsNotCalledTwice(spy: MotionEvent) { - // Spy in mockito will create copy of the spied object, copying all its field etc. Here it means - // we create copy of MotionEvent and its mNativePtr, so we have two separate objects of type - // MotionEvents with the same mNativePtr. That breaks because MotionEvent has custom finalize() - // method which goes to native code and tries to delete the reference from mNativePtr. It works - // first time but second time reference is already deleted and it breaks. That's why we have to - // avoid calling finalize twice - doNothing().whenever(spy).finalizeUsingReflection() -} - -private fun MotionEvent.finalizeUsingReflection() { - val finalizeMethod: Method = MotionEvent::class.java.getDeclaredMethod("finalize") - finalizeMethod.isAccessible = true - finalizeMethod.invoke(this) + // we need to use mock with delegation instead of spy because: + // 1. Spy will try to deallocate the same memory again when finalize() is called as it keep the + // same memory pointer to native MotionEvent + // 2. Even after workaround for issue above there still remains problem with destructor of + // native event trying to free the same chunk of native memory. I'm not sure why it happens but + // mock seems to fix the issue and because it delegates all calls seems safer overall + val delegate = mock(MotionEvent::class.java, AdditionalAnswers.delegatesTo<MotionEvent>(event)) + doReturn(toolType).whenever(delegate).getToolType(0) + doReturn(pointerCount).whenever(delegate).pointerCount + doReturn(classification).whenever(delegate).classification + axisValues.forEach { (key, value) -> doReturn(value).whenever(delegate).getAxisValue(key) } + return delegate } fun touchpadEvent( diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index f7371487a7c5..3f5dc82ec5cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -18,6 +18,8 @@ package com.android.systemui.volume; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -37,16 +39,19 @@ import android.media.IAudioService; import android.media.session.MediaSession; import android.os.Handler; import android.os.Process; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.settingslib.flags.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; @@ -54,7 +59,9 @@ import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor; import org.junit.Before; import org.junit.Test; @@ -63,6 +70,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Objects; import java.util.concurrent.Executor; @RunWith(AndroidJUnit4.class) @@ -104,6 +112,10 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private DumpManager mDumpManager; + @Mock + private AudioSharingInteractor mAudioSharingInteractor; + @Mock + private JavaAdapter mJavaAdapter; @Before @@ -124,11 +136,26 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mCallback = mock(VolumeDialogControllerImpl.C.class); mThreadFactory.setLooper(TestableLooper.get(this).getLooper()); - mVolumeController = new TestableVolumeDialogControllerImpl(mContext, - mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager, - mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager, - mPackageManager, mWakefullnessLifcycle, mKeyguardManager, - mActivityManager, mUserTracker, mDumpManager, mCallback); + mVolumeController = + new TestableVolumeDialogControllerImpl( + mContext, + mBroadcastDispatcher, + mRingerModeTracker, + mThreadFactory, + mAudioManager, + mNotificationManager, + mVibrator, + mIAudioService, + mAccessibilityManager, + mPackageManager, + mWakefullnessLifcycle, + mKeyguardManager, + mActivityManager, + mUserTracker, + mDumpManager, + mCallback, + mAudioSharingInteractor, + mJavaAdapter); mVolumeController.setEnableDialogs(true, true); } @@ -224,6 +251,41 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class)); } + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void handleAudioSharingStreamVolumeChanges_updateState() { + ArgumentCaptor<VolumeDialogController.State> stateCaptor = + ArgumentCaptor.forClass(VolumeDialogController.State.class); + int broadcastStream = VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; + + mVolumeController.handleAudioSharingStreamVolumeChanges(100); + + verify(mCallback).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue(); + assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(100); + + mVolumeController.handleAudioSharingStreamVolumeChanges(200); + + verify(mCallback, times(2)).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue(); + assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(200); + + mVolumeController.handleAudioSharingStreamVolumeChanges(null); + + verify(mCallback, times(3)).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void testSetStreamVolume_setSecondaryDeviceVolume() { + mVolumeController.setStreamVolume( + VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST, /* level= */ 100); + Objects.requireNonNull(TestableLooper.get(this)).processAllMessages(); + + verify(mAudioSharingInteractor).setStreamVolume(100); + } + static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl { private final WakefulnessLifecycle.Observer mWakefullessLifecycleObserver; @@ -243,11 +305,27 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { ActivityManager activityManager, UserTracker userTracker, DumpManager dumpManager, - C callback) { - super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager, - notificationManager, optionalVibrator, iAudioService, accessibilityManager, - packageManager, wakefulnessLifecycle, keyguardManager, - activityManager, userTracker, dumpManager); + C callback, + AudioSharingInteractor audioSharingInteractor, + JavaAdapter javaAdapter) { + super( + context, + broadcastDispatcher, + ringerModeTracker, + theadFactory, + audioManager, + notificationManager, + optionalVibrator, + iAudioService, + accessibilityManager, + packageManager, + wakefulnessLifecycle, + keyguardManager, + activityManager, + userTracker, + dumpManager, + audioSharingInteractor, + javaAdapter); mCallbacks = callback; ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index cdfcca6c7065..b5cbf598bd05 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -23,6 +23,7 @@ import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER; import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN; +import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS; import static junit.framework.Assert.assertEquals; @@ -72,6 +73,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.settingslib.flags.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; @@ -794,6 +796,38 @@ public class VolumeDialogImplTest extends SysuiTestCase { verify(mVolumeDialogInteractor, atLeastOnce()).onDialogDismissed(); // dismiss by timeout } + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void testDynamicStreamForBroadcast_createRow() { + State state = createShellState(); + VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState(); + ss.dynamic = true; + ss.levelMin = 0; + ss.levelMax = 255; + ss.level = 20; + ss.name = -1; + ss.remoteLabel = mContext.getString(R.string.audio_sharing_description); + state.states.append(DYNAMIC_STREAM_BROADCAST, ss); + + mDialog.onStateChangedH(state); + mTestableLooper.processAllMessages(); + + ViewGroup volumeDialogRows = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows); + assumeNotNull(volumeDialogRows); + View broadcastRow = null; + final int rowCount = volumeDialogRows.getChildCount(); + // we don't make assumptions about the position of the dnd row + for (int i = 0; i < rowCount; i++) { + View volumeRow = volumeDialogRows.getChildAt(i); + if (volumeRow.getId() == DYNAMIC_STREAM_BROADCAST) { + broadcastRow = volumeRow; + break; + } + } + assertNotNull(broadcastRow); + assertEquals(broadcastRow.getVisibility(), View.VISIBLE); + } + /** * @return true if at least one volume row has the DND icon */ diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt new file mode 100644 index 000000000000..2050437da38a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.systemui.communal.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.communalSceneTransitionRepository: CommunalSceneTransitionRepository by + Kosmos.Fixture { CommunalSceneTransitionRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt index 1da1fb2ee52f..5e870b19681b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt @@ -25,20 +25,11 @@ import kotlinx.coroutines.flow.map /** Fake implementation of [CommunalPrefsRepository] */ class FakeCommunalPrefsRepository : CommunalPrefsRepository { private val _isCtaDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) - private val _isDisclaimerDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = _isCtaDismissed.map { it.contains(user) } - override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> = - _isDisclaimerDismissed.map { it.contains(user) } - override suspend fun setCtaDismissed(user: UserInfo) { _isCtaDismissed.value = _isCtaDismissed.value.toMutableSet().apply { add(user) } } - - override suspend fun setDisclaimerDismissed(user: UserInfo) { - _isDisclaimerDismissed.value = - _isDisclaimerDismissed.value.toMutableSet().apply { add(user) } - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt index d280be216f5e..82454817ecbb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt @@ -6,7 +6,6 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.communal.shared.model.CommunalScenes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -25,11 +24,10 @@ class FakeCommunalSceneRepository( ) : CommunalSceneRepository { override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = - snapToScene(toScene, 0) + snapToScene(toScene) - override fun snapToScene(toScene: SceneKey, delayMillis: Long) { + override fun snapToScene(toScene: SceneKey) { applicationScope.launch { - delay(delayMillis) currentScene.value = toScene _transitionState.value = flowOf(ObservableTransitionState.Idle(toScene)) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index eb9278537db5..4ad046cc095e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -42,6 +43,7 @@ val Kosmos.communalInteractor by Fixture { CommunalInteractor( applicationScope = applicationCoroutineScope, bgDispatcher = testDispatcher, + bgScope = testScope.backgroundScope, broadcastDispatcher = broadcastDispatcher, communalSceneInteractor = communalSceneInteractor, widgetRepository = communalWidgetRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..e6e59e1a523e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.systemui.communal.domain.interactor + +import com.android.systemui.communal.data.repository.communalSceneTransitionRepository +import com.android.systemui.keyguard.domain.interactor.internalKeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope + +val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by + Kosmos.Fixture { + CommunalSceneTransitionInteractor( + applicationScope = applicationCoroutineScope, + transitionInteractor = keyguardTransitionInteractor, + internalTransitionInteractor = internalKeyguardTransitionInteractor, + settingsInteractor = communalSettingsInteractor, + sceneInteractor = communalSceneInteractor, + repository = communalSceneTransitionRepository, + keyguardInteractor = keyguardInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt index f73f43dc53fe..edf4bcc238c0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt @@ -17,7 +17,10 @@ package com.android.systemui.education.data.repository import com.android.systemui.kosmos.Kosmos +import java.time.Clock import java.time.Instant var Kosmos.contextualEducationRepository: ContextualEducationRepository by - Kosmos.Fixture { FakeContextualEducationRepository(FakeEduClock(Instant.MIN)) } + Kosmos.Fixture { FakeContextualEducationRepository(fakeEduClock) } + +var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt new file mode 100644 index 000000000000..5b2dc2b39e27 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.systemui.education.domain.interactor + +import com.android.systemui.education.data.repository.contextualEducationRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.user.domain.interactor.selectedUserInteractor + +val Kosmos.contextualEducationInteractor by + Kosmos.Fixture { + ContextualEducationInteractor( + backgroundScope = testScope.backgroundScope, + repository = contextualEducationRepository, + selectedUserInteractor = selectedUserInteractor + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt new file mode 100644 index 000000000000..8f84e0482b83 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -0,0 +1,28 @@ +/* + * 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.systemui.education.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope + +var Kosmos.keyboardTouchpadEduStatsInteractor by + Kosmos.Fixture { + KeyboardTouchpadEduStatsInteractorImpl( + backgroundScope = testScope.backgroundScope, + contextualEducationInteractor = contextualEducationInteractor + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt index 079852a641c2..494f08b88345 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -38,5 +39,6 @@ var Kosmos.fromGlanceableHubTransitionInteractor by transitionInteractor = keyguardTransitionInteractor, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + communalSceneInteractor = communalSceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt index c2169456eac5..7827655806d8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -37,5 +38,6 @@ val Kosmos.fromOccludedTransitionInteractor by powerInteractor = powerInteractor, communalInteractor = communalInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + communalSceneInteractor = communalSceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt index da956ec67696..8b4de2bcc26f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt @@ -22,13 +22,12 @@ package com.android.systemui.statusbar.notification.collection * The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any * modifications made to the builder will be applied to the [entry]. */ -inline fun modifyEntry( - entry: NotificationEntry, +inline fun NotificationEntry.modifyEntry( crossinline modifier: NotificationEntryBuilder.() -> Unit ) { - val builder = NotificationEntryBuilder(entry) + val builder = NotificationEntryBuilder(this) modifier(builder) - builder.apply(entry) + builder.apply(this) } fun getAttachState(entry: ListEntry): ListAttachState { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt new file mode 100644 index 000000000000..77d97bb7cbe9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt @@ -0,0 +1,42 @@ +/* + * 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.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor +import com.android.systemui.util.settings.fakeSettings + +var Kosmos.lockScreenMinimalismCoordinator by + Kosmos.Fixture { + LockScreenMinimalismCoordinator( + bgDispatcher = testDispatcher, + dumpManager = dumpManager, + headsUpInteractor = headsUpNotificationInteractor, + logger = lockScreenMinimalismCoordinatorLogger, + scope = testScope.backgroundScope, + secureSettings = fakeSettings, + seenNotificationsInteractor = seenNotificationsInteractor, + statusBarStateController = statusBarStateController, + shadeInteractor = shadeInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt new file mode 100644 index 000000000000..77aeb44e15eb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.lockScreenMinimalismCoordinatorLogger by + Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt index 492e87bbcac4..7e8f1a9115ea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt @@ -22,14 +22,19 @@ import com.android.systemui.statusbar.notification.data.repository.HeadsUpReposi import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() } class FakeHeadsUpNotificationRepository : HeadsUpRepository { override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false) - override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null) - override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> = - MutableStateFlow(emptySet()) + + val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>()) + override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = + orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged() + override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> = + orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged() override fun setHeadsUpAnimatingAway(animatingAway: Boolean) { isHeadsUpAnimatingAway.value = animatingAway @@ -38,4 +43,12 @@ class FakeHeadsUpNotificationRepository : HeadsUpRepository { override fun snooze() { // do nothing } + + fun setNotifications(notifications: List<HeadsUpRowRepository>) { + this.orderedHeadsUpRows.value = notifications.toList() + } + + fun setNotifications(vararg notifications: HeadsUpRowRepository) { + this.orderedHeadsUpRows.value = notifications.toList() + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt index 9247e889f8f9..e3176f1783e3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt @@ -26,9 +26,14 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository { private val _isDeviceProvisioned = MutableStateFlow(true) override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned + fun setDeviceProvisioned(isProvisioned: Boolean) { _isDeviceProvisioned.value = isProvisioned } + + override fun isDeviceProvisioned(): Boolean { + return _isDeviceProvisioned.value + } } @Module diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index b4efae3a05e4..8e2e0ad76d15 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -114,6 +114,13 @@ flag { } flag { + name: "enable_magnification_follows_mouse" + namespace: "accessibility" + description: "Whether to enable mouse following for fullscreen magnification" + bug: "335494097" +} + +flag { name: "fix_drag_pointer_when_ending_drag" namespace: "accessibility" description: "Send the correct pointer id when transitioning from dragging to delegating states." diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index b918d80fc63d..30c743e508b6 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -161,6 +161,7 @@ import android.view.inputmethod.EditorInfo; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.accessibility.AccessibilityShortcutController.ExtraDimFrameworkFeatureInfo; import com.android.internal.accessibility.AccessibilityShortcutController.FrameworkFeatureInfo; import com.android.internal.accessibility.AccessibilityShortcutController.LaunchableFrameworkFeatureInfo; import com.android.internal.accessibility.common.ShortcutConstants; @@ -3910,6 +3911,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Slog.d(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName); return; } + // In case user assigned an accessibility framework feature to the given shortcut. if (performAccessibilityFrameworkFeature(displayId, targetComponentName, shortcutType)) { return; @@ -3933,6 +3935,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (!frameworkFeatureMap.containsKey(assignedTarget)) { return false; } + final int userId; + synchronized (mLock) { + userId = mCurrentUserId; + } final FrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(assignedTarget); final SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(), featureInfo.getSettingKey(), mCurrentUserId); @@ -3944,6 +3950,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return true; } + if (featureInfo instanceof ExtraDimFrameworkFeatureInfo) { + boolean serviceEnabled = + ((ExtraDimFrameworkFeatureInfo) featureInfo) + .activateShortcut(mContext, userId); + logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType, + serviceEnabled); + return true; + } + // Assuming that the default state will be to have the feature off if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) { logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType, diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index ad8f5e1481e1..668852b46ea2 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -31,12 +31,14 @@ import android.hardware.display.DisplayManager; import android.metrics.LogMaker; import android.os.UserManager; import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; import android.service.autofill.InternalSanitizer; import android.service.autofill.SaveInfo; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; +import android.util.SparseArray; import android.view.Display; import android.view.View; import android.view.WindowManager; @@ -374,4 +376,50 @@ public final class Helper { private interface ViewNodeFilter { boolean matches(ViewNode node); } + + public static class SaveInfoStats { + public int saveInfoCount; + public int saveDataTypeCount; + + public SaveInfoStats(int saveInfoCount, int saveDataTypeCount) { + this.saveInfoCount = saveInfoCount; + this.saveDataTypeCount = saveDataTypeCount; + } + } + + /** + * Get statistic information of save info given a sparse array of fill responses. + * + * Specifically the statistic includes + * 1. how many save info the current session has. + * 2. How many distinct save data types current session has. + * + * @return SaveInfoStats returns the above two number in a SaveInfoStats object + */ + public static SaveInfoStats getSaveInfoStatsFromFillResponses( + SparseArray<FillResponse> fillResponses) { + if (fillResponses == null) { + if (sVerbose) { + Slog.v(TAG, "getSaveInfoStatsFromFillResponses(): fillResponse sparse array is " + + "null"); + } + return new SaveInfoStats(-1, -1); + } + int numSaveInfos = 0; + int numSaveDataTypes = 0; + ArraySet<Integer> saveDataTypeSeen = new ArraySet<>(); + final int numResponses = fillResponses.size(); + for (int responseNum = 0; responseNum < numResponses; responseNum++) { + final FillResponse response = fillResponses.valueAt(responseNum); + if (response != null && response.getSaveInfo() != null) { + numSaveInfos += 1; + int saveDataType = response.getSaveInfo().getType(); + if (!saveDataTypeSeen.contains(saveDataType)) { + saveDataTypeSeen.add(saveDataType); + numSaveDataTypes += 1; + } + } + } + return new SaveInfoStats(numSaveInfos, numSaveDataTypes); + } } diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index a5ec2ba2f267..49ca29745b11 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -854,7 +854,6 @@ public final class PresentationStatsEventLogger { mCallingAppUid, event.mIsCredentialRequest, event.mWebviewRequestedCredential, - event.mFilteredFillabaleViewCount, event.mViewFillableTotalCount, event.mViewFillFailureCount, event.mFocusedId, @@ -868,6 +867,7 @@ public final class PresentationStatsEventLogger { event.mFocusedVirtualAutofillId, event.mFieldFirstLength, event.mFieldLastLength, + event.mFilteredFillabaleViewCount, event.mViewFailedPriorToRefillCount, event.mViewFilledSuccessfullyOnRefillCount, event.mViewFailedOnRefillCount, diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index c6ddc16211bc..21df7a5487ef 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -70,6 +70,7 @@ import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATU import static com.android.server.autofill.Helper.containsCharsInOrder; import static com.android.server.autofill.Helper.createSanitizers; import static com.android.server.autofill.Helper.getNumericValue; +import static com.android.server.autofill.Helper.SaveInfoStats; import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; import static com.android.server.autofill.Helper.toArray; @@ -3203,11 +3204,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return saveInfo == null ? 0 : saveInfo.getFlags(); } - static class SaveInfoStats { - public int saveInfoCount; - public int saveDataTypeCount; - } - /** * Get statistic information of save info in current session. Specifically * 1. how many save info the current session has. @@ -3217,42 +3213,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState */ @GuardedBy("mLock") private SaveInfoStats getSaveInfoStatsLocked() { - SaveInfoStats retSaveInfoStats = new SaveInfoStats(); - retSaveInfoStats.saveInfoCount = -1; - retSaveInfoStats.saveDataTypeCount = -1; - if (mContexts == null) { if (sVerbose) { Slog.v(TAG, "getSaveInfoStatsLocked(): mContexts is null"); } - } else if (mResponses == null) { - // Happens when the activity / session was finished before the service replied, or - // when the service cannot autofill it (and returned a null response). - if (sVerbose) { - Slog.v(TAG, "getSaveInfoStatsLocked(): mResponses is null"); - } - return retSaveInfoStats; - } else { - int numSaveInfos = 0; - int numSaveDataTypes = 0; - ArraySet<Integer> saveDataTypeSeen = new ArraySet<>(); - final int numResponses = mResponses.size(); - for (int responseNum = 0; responseNum < numResponses; responseNum++) { - final FillResponse response = mResponses.valueAt(responseNum); - if (response != null && response.getSaveInfo() != null) { - numSaveInfos += 1; - int saveDataType = response.getSaveInfo().getType(); - if (!saveDataTypeSeen.contains(saveDataType)) { - saveDataTypeSeen.add(saveDataType); - numSaveDataTypes += 1; - } - } - } - retSaveInfoStats.saveInfoCount = numSaveInfos; - retSaveInfoStats.saveDataTypeCount = numSaveDataTypes; + return new SaveInfoStats(-1, -1); } - - return retSaveInfoStats; + return Helper.getSaveInfoStatsFromFillResponses(mResponses); } /** diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 3d53deb8d2bb..4fc9d55d3e17 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -103,9 +103,10 @@ class CompanionDeviceShellCommand extends ShellCommand { String packageName = getNextArgRequired(); String address = getNextArgRequired(); String deviceProfile = getNextArg(); + boolean selfManaged = getNextBooleanArg(); final MacAddress macAddress = MacAddress.fromString(address); mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - deviceProfile, deviceProfile, /* associatedDevice */ null, false, + deviceProfile, deviceProfile, /* associatedDevice */ null, selfManaged, /* callback */ null, /* resultReceiver */ null); } break; @@ -462,6 +463,17 @@ class CompanionDeviceShellCommand extends ShellCommand { } } + private boolean getNextBooleanArg() { + String arg = getNextArg(); + if (arg == null || "false".equalsIgnoreCase(arg)) { + return false; + } else if ("true".equalsIgnoreCase(arg)) { + return Boolean.parseBoolean(arg); + } else { + throw new IllegalArgumentException("Expected a boolean argument but was: " + arg); + } + } + @Override public void onHelp() { PrintWriter pw = getOutPrintWriter(); @@ -470,7 +482,7 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Print this help text."); pw.println(" list USER_ID"); pw.println(" List all Associations for a user."); - pw.println(" associate USER_ID PACKAGE MAC_ADDRESS [DEVICE_PROFILE]"); + pw.println(" associate USER_ID PACKAGE MAC_ADDRESS [DEVICE_PROFILE] [SELF_MANAGED]"); pw.println(" Create a new Association."); pw.println(" disassociate USER_ID PACKAGE MAC_ADDRESS"); pw.println(" Remove an existing Association."); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 7d9d660af536..ee7d0aef2189 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -28,9 +28,9 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; -import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix; +import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import android.annotation.EnforcePermission; import android.annotation.NonNull; @@ -561,8 +561,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private void sendPendingIntent(int displayId, PendingIntent pendingIntent) throws PendingIntent.CanceledException { final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId); - options.setPendingIntentBackgroundActivityLaunchAllowed(true); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); pendingIntent.send( mContext, /* code= */ 0, diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 69ee8fc831f4..cf0befaab98d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -16961,16 +16961,24 @@ public class ActivityManagerService extends IActivityManager.Stub int userId = UserHandle.getCallingUserId(); - if (UserManager.isVisibleBackgroundUsersEnabled() && userId != getCurrentUserId()) { - // The check is added mainly for auto devices. On auto devices, it is possible that - // multiple users are visible simultaneously using visible background users. - // In such cases, it is desired that only the current user (not the visible background - // user) can change the locale and other persistent settings of the device. - Slog.w(TAG, "Only current user is allowed to update persistent configuration if " - + "visible background users are enabled. Current User" + getCurrentUserId() - + ". Calling User: " + userId); - throw new SecurityException("Only current user is allowed to update persistent " - + "configuration."); + if (UserManager.isVisibleBackgroundUsersEnabled()) { + final long origId = Binder.clearCallingIdentity(); + try { + if (userId != getCurrentUserId()) { + // The check is added mainly for auto devices. On auto devices, it is + // possible that multiple users are visible simultaneously using visible + // background users. In such cases, it is desired that only the current user + // (not the visible background user) can change the locale and other persistent + // settings of the device. + Slog.w(TAG, "Only current user is allowed to update persistent configuration " + + "if visible background users are enabled. Current User" + + getCurrentUserId() + ". Calling User: " + userId); + throw new SecurityException("Only current user is allowed to update persistent " + + "configuration."); + } + } finally { + Binder.restoreCallingIdentity(origId); + } } mActivityTaskManager.updatePersistentConfiguration(values, userId); diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index 061bcd740f6b..ee7033e4a437 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -47,6 +47,7 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; @@ -63,6 +64,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.server.LocalServices; import com.android.server.PackageWatchdog; +import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.UserManagerService; import com.android.server.usage.AppStandbyInternal; import com.android.server.wm.WindowProcessController; @@ -868,9 +871,6 @@ class AppErrors { private boolean handleAppCrashLSPB(ProcessRecord app, String reason, String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) { final long now = SystemClock.uptimeMillis(); - final boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.ANR_SHOW_BACKGROUND, 0, - mService.mUserController.getCurrentUserId()) != 0; Long crashTime; Long crashTimePersistent; @@ -881,6 +881,8 @@ class AppErrors { final boolean persistent = app.isPersistent(); final WindowProcessController proc = app.getWindowProcessController(); final ProcessErrorStateRecord errState = app.mErrorState; + final boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ANR_SHOW_BACKGROUND, 0, getVisibleUserId(userId)) != 0; if (!app.isolated) { crashTime = mProcessCrashTimes.get(processName, uid); @@ -1000,9 +1002,6 @@ class AppErrors { void handleShowAppErrorUi(Message msg) { AppErrorDialog.Data data = (AppErrorDialog.Data) msg.obj; - boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.ANR_SHOW_BACKGROUND, 0, - mService.mUserController.getCurrentUserId()) != 0; final int userId; synchronized (mProcLock) { @@ -1027,7 +1026,11 @@ class AppErrors { for (int profileId : mService.mUserController.getCurrentProfileIds()) { isBackground &= (userId != profileId); } - if (isBackground && !showBackground) { + int visibleUserId = getVisibleUserId(userId); + boolean isVisibleUser = isVisibleBackgroundUser(visibleUserId); + boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0; + if (isBackground && !showBackground && !isVisibleUser) { Slog.w(TAG, "Skipping crash dialog of " + proc + ": background"); if (res != null) { res.set(AppErrorDialog.BACKGROUND_USER); @@ -1054,7 +1057,7 @@ class AppErrors { final long now = SystemClock.uptimeMillis(); final boolean shouldThottle = crashShowErrorTime != null && now < crashShowErrorTime + ActivityManagerConstants.MIN_CRASH_INTERVAL; - if ((mService.mAtmInternal.canShowErrorDialogs() || showBackground) + if ((mService.mAtmInternal.canShowErrorDialogs(visibleUserId) || showBackground) && !crashSilenced && !shouldThottle && (showFirstCrash || showFirstCrashDevOption || data.repeating)) { Slog.i(TAG, "Showing crash dialog for package " + packageName + " u" + userId); @@ -1103,10 +1106,10 @@ class AppErrors { return; } + int visibleUserId = getVisibleUserId(proc.userId); boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.ANR_SHOW_BACKGROUND, 0, - mService.mUserController.getCurrentUserId()) != 0; - if (mService.mAtmInternal.canShowErrorDialogs() || showBackground) { + Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0; + if (mService.mAtmInternal.canShowErrorDialogs(visibleUserId) || showBackground) { AnrController anrController = errState.getDialogController().getAnrController(); if (anrController == null) { errState.getDialogController().showAnrDialogs(data); @@ -1163,6 +1166,43 @@ class AppErrors { } /** + * Returns the user ID of the visible user associated with the error occurrence. + * + * <p>For most cases it will return the current foreground user ID, but on devices that + * {@link UserManager#isVisibleBackgroundUsersEnabled() support visible background users}, + * it will return the given app user ID passed as parameter. + * + * @param appUserId The user ID of the app where the error occurred. + * @return The ID of the visible user associated with the error. + */ + private int getVisibleUserId(int appUserId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return mService.mUserController.getCurrentUserId(); + } + return appUserId; + } + + /** + * Checks if the given user is a visible background user, which is a full, background user + * assigned to secondary displays on the devices that have + * {@link UserManager#isVisibleBackgroundUsersEnabled() + * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on + * automotive builds, using the display associated with their seats). + * + * @see UserManager#isUserVisible() + */ + private boolean isVisibleBackgroundUser(int userId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return false; + } + boolean isForeground = mService.mUserController.getCurrentUserId() == userId; + boolean isProfile = UserManagerService.getInstance().isProfile(userId); + boolean isVisible = LocalServices.getService(UserManagerInternal.class) + .isUserVisible(userId); + return isVisible && !isForeground && !isProfile; + } + + /** * Information about a process that is currently marked as bad. */ static final class BadProcessInfo { diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 67985efcd7bc..8df4e7702be8 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -102,6 +102,7 @@ import android.util.StatsEvent; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; import com.android.internal.os.BinderCallsStats; import com.android.internal.os.Clock; @@ -671,6 +672,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub BatteryConsumer.POWER_COMPONENT_FLASHLIGHT, Flags.streamlinedMiscBatteryStats()); + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_GNSS, + Flags.streamlinedMiscBatteryStats()); + mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( + BatteryConsumer.POWER_COMPONENT_GNSS, + Flags.streamlinedMiscBatteryStats()); + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CAMERA, Flags.streamlinedMiscBatteryStats()); mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( @@ -1097,18 +1104,20 @@ public final class BatteryStatsService extends IBatteryStats.Stub final StatsManager statsManager = mContext.getSystemService(StatsManager.class); final StatsPullAtomCallbackImpl pullAtomCallback = new StatsPullAtomCallbackImpl(); - statsManager.setPullAtomCallback( - FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET, - null, // use default PullAtomMetadata values - DIRECT_EXECUTOR, pullAtomCallback); - statsManager.setPullAtomCallback( - FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL, - null, // use default PullAtomMetadata values - DIRECT_EXECUTOR, pullAtomCallback); - statsManager.setPullAtomCallback( - FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET, - null, // use default PullAtomMetadata values - DIRECT_EXECUTOR, pullAtomCallback); + if (!Flags.disableCompositeBatteryUsageStatsAtoms()) { + statsManager.setPullAtomCallback( + FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, pullAtomCallback); + statsManager.setPullAtomCallback( + FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, pullAtomCallback); + statsManager.setPullAtomCallback( + FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, pullAtomCallback); + } if (Flags.addBatteryUsageStatsSliceAtom()) { statsManager.setPullAtomCallback( FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID, @@ -1125,6 +1134,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub final BatteryUsageStats bus; switch (atomTag) { case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET: { + if (Flags.disableCompositeBatteryUsageStatsAtoms()) { + return StatsManager.PULL_SKIP; + } + @SuppressLint("MissingPermission") final double minConsumedPowerThreshold = DeviceConfig.getFloat(DEVICE_CONFIG_NAMESPACE, @@ -1141,6 +1154,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub break; } case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL: + if (Flags.disableCompositeBatteryUsageStatsAtoms()) { + return StatsManager.PULL_SKIP; + } + final BatteryUsageStatsQuery queryPowerProfile = new BatteryUsageStatsQuery.Builder() .setMaxStatsAgeMs(0) @@ -1152,6 +1169,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub bus = getBatteryUsageStats(List.of(queryPowerProfile)).get(0); break; case FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET: { + if (Flags.disableCompositeBatteryUsageStatsAtoms()) { + return StatsManager.PULL_SKIP; + } + final long sessionStart = getLastBatteryUsageStatsBeforeResetAtomPullTimestamp(); final long sessionEnd; @@ -1191,7 +1212,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub .setMinConsumedPowerThreshold(minConsumedPowerThreshold) .build(); bus = getBatteryUsageStats(List.of(query)).get(0); - return StatsPerUidLogger.logStats(bus, data); + return new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data); } default: throw new UnsupportedOperationException("Unknown tagId=" + atomTag); @@ -1204,7 +1225,35 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } - private static class StatsPerUidLogger { + public static class FrameworkStatsLogger { + /** + * Wrapper for the FrameworkStatsLog.buildStatsEvent method that makes it easier + * for mocking. + */ + @VisibleForTesting + public StatsEvent buildStatsEvent(long sessionStartTs, long sessionEndTs, + long sessionDuration, int sessionDischargePercentage, long sessionDischargeDuration, + int uid, @BatteryConsumer.ProcessState int processState, long timeInStateMillis, + String powerComponentName, float totalConsumedPowerMah, float powerComponentMah, + long powerComponentDurationMillis) { + return FrameworkStatsLog.buildStatsEvent( + FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID, + sessionStartTs, + sessionEndTs, + sessionDuration, + sessionDischargePercentage, + sessionDischargeDuration, + uid, + processState, + timeInStateMillis, + powerComponentName, + totalConsumedPowerMah, + powerComponentMah, + powerComponentDurationMillis); + } + } + + public static class StatsPerUidLogger { private static final int STATSD_METRIC_MAX_DIMENSIONS_COUNT = 3000; @@ -1224,7 +1273,18 @@ public final class BatteryStatsService extends IBatteryStats.Stub long dischargeDuration) {} ; - static int logStats(BatteryUsageStats bus, List<StatsEvent> data) { + private final FrameworkStatsLogger mFrameworkStatsLogger; + + public StatsPerUidLogger(FrameworkStatsLogger frameworkStatsLogger) { + mFrameworkStatsLogger = frameworkStatsLogger; + } + + /** + * Generates StatsEvents for the supplied battery usage stats and adds them to + * the supplied list. + */ + @VisibleForTesting + public int logStats(BatteryUsageStats bus, List<StatsEvent> data) { final SessionInfo sessionInfo = new SessionInfo( bus.getStatsStartTimestamp(), @@ -1340,7 +1400,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub return StatsManager.PULL_SUCCESS; } - private static boolean addStatsForPredefinedComponent( + private boolean addStatsForPredefinedComponent( List<StatsEvent> data, SessionInfo sessionInfo, int uid, @@ -1380,7 +1440,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub powerComponentDurationMillis); } - private static boolean addStatsForCustomComponent( + private boolean addStatsForCustomComponent( List<StatsEvent> data, SessionInfo sessionInfo, int uid, @@ -1422,7 +1482,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub * Returns true on success and false if reached max atoms capacity and no more atoms should * be added */ - private static boolean addStatsAtom( + private boolean addStatsAtom( List<StatsEvent> data, SessionInfo sessionInfo, int uid, @@ -1432,9 +1492,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub float totalConsumedPowerMah, float powerComponentMah, long powerComponentDurationMillis) { - data.add( - FrameworkStatsLog.buildStatsEvent( - FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID, + data.add(mFrameworkStatsLogger.buildStatsEvent( sessionInfo.startTs(), sessionInfo.endTs(), sessionInfo.duration(), @@ -3214,6 +3272,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub .setMaxStatsAgeMs(0) .includeProcessStateData() .includePowerModels(); + if (Flags.batteryUsageStatsByPowerAndScreenState()) { + builder.includeScreenStateData().includePowerStateData(); + } if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) { builder.powerProfileModeledOnly(); } @@ -3232,7 +3293,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub if (proto) { batteryUsageStats.dumpToProto(fd); } else { - batteryUsageStats.dump(pw, ""); + batteryUsageStats.dump(pw, " "); } } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index b058bd8eb3d6..504c54aefc44 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1708,6 +1708,11 @@ public class OomAdjuster { // priority for this non-top split. schedGroup = SCHED_GROUP_TOP_APP; mAdjType = "resumed-split-screen-activity"; + } else if ((flags + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) { + // The recently used non-top visible freeform app. + schedGroup = SCHED_GROUP_TOP_APP; + mAdjType = "perceptible-freeform-activity"; } foregroundActivities = true; mHasVisibleActivities = true; diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index f6df60f736e8..e4c65bd2147d 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -269,7 +269,8 @@ public final class GameManagerService extends IGameManagerService.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver result) { - new GameManagerShellCommand().exec(this, in, out, err, args, callback, result); + new GameManagerShellCommand(mPackageManager).exec(this, in, out, err, args, callback, + result); } @Override diff --git a/services/core/java/com/android/server/app/GameManagerShellCommand.java b/services/core/java/com/android/server/app/GameManagerShellCommand.java index ab57c4fe837e..d3b4312bf4a3 100644 --- a/services/core/java/com/android/server/app/GameManagerShellCommand.java +++ b/services/core/java/com/android/server/app/GameManagerShellCommand.java @@ -16,10 +16,13 @@ package com.android.server.app; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.GameManager; import android.app.IGameManagerService; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -47,7 +50,10 @@ public class GameManagerShellCommand extends ShellCommand { private static final String UNSUPPORTED_MODE_NUM = String.valueOf( GameManager.GAME_MODE_UNSUPPORTED); - public GameManagerShellCommand() { + private PackageManager mPackageManager; + + public GameManagerShellCommand(@NonNull PackageManager packageManager) { + mPackageManager = packageManager; } @Override @@ -91,9 +97,29 @@ public class GameManagerShellCommand extends ShellCommand { return -1; } + private boolean isPackageGame(String packageName, int userId, PrintWriter pw) { + try { + final ApplicationInfo applicationInfo = mPackageManager + .getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId); + boolean isGame = applicationInfo.category == ApplicationInfo.CATEGORY_GAME; + if (!isGame) { + pw.println("Package " + packageName + " is not of game type, to use the game " + + "mode commands, it must specify game category in the manifest as " + + "android:appCategory=\"game\""); + } + return isGame; + } catch (PackageManager.NameNotFoundException e) { + pw.println("Package " + packageName + " is not found for user " + userId); + return false; + } + } + private int runListGameModes(PrintWriter pw) throws ServiceNotFoundException, RemoteException { final String packageName = getNextArgRequired(); final int userId = ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); final String currentMode = gameModeIntToString( @@ -110,12 +136,15 @@ public class GameManagerShellCommand extends ShellCommand { private int runListGameModeConfigs(PrintWriter pw) throws ServiceNotFoundException, RemoteException { final String packageName = getNextArgRequired(); - + final int userId = ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); final String listStr = gameManagerService.getInterventionList(packageName, - ActivityManager.getCurrentUser()); + userId); if (listStr == null) { pw.println("No interventions found for " + packageName); @@ -131,15 +160,17 @@ public class GameManagerShellCommand extends ShellCommand { if (option != null && option.equals("--user")) { userIdStr = getNextArgRequired(); } - final String gameMode = getNextArgRequired(); final String packageName = getNextArgRequired(); + int userId = userIdStr != null ? Integer.parseInt(userIdStr) + : ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final IGameManagerService service = IGameManagerService.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.GAME_SERVICE)); boolean batteryModeSupported = false; boolean perfModeSupported = false; - int userId = userIdStr != null ? Integer.parseInt(userIdStr) - : ActivityManager.getCurrentUser(); int[] modes = service.getAvailableGameModes(packageName, userId); for (int mode : modes) { if (mode == GameManager.GAME_MODE_PERFORMANCE) { @@ -262,6 +293,9 @@ public class GameManagerShellCommand extends ShellCommand { int userId = userIdStr != null ? Integer.parseInt(userIdStr) : ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); @@ -308,13 +342,14 @@ public class GameManagerShellCommand extends ShellCommand { } final String packageName = getNextArgRequired(); - - final GameManagerService gameManagerService = (GameManagerService) - ServiceManager.getService(Context.GAME_SERVICE); - int userId = userIdStr != null ? Integer.parseInt(userIdStr) : ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } + final GameManagerService gameManagerService = (GameManagerService) + ServiceManager.getService(Context.GAME_SERVICE); if (gameMode == null) { gameManagerService.resetGameModeConfigOverride(packageName, userId, -1); return 0; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index fab0a56af2a8..fe3bbb094284 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -739,6 +739,8 @@ public class AudioService extends IAudioService.Stub // Broadcast receiver for device connections intent broadcasts private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver(); + private final Executor mAudioServerLifecycleExecutor; + private IMediaProjectionManager mProjectionService; // to validate projection token /** Interface for UserManagerService. */ @@ -1059,7 +1061,8 @@ public class AudioService extends IAudioService.Stub audioserverPermissions() ? initializeAudioServerPermissionProvider( context, audioPolicyFacade, audioserverLifecycleExecutor) : - null + null, + audioserverLifecycleExecutor ); } @@ -1145,13 +1148,16 @@ public class AudioService extends IAudioService.Stub * {@link AudioSystemThread} is created as the messaging thread instead. * @param appOps {@link AppOpsManager} system service * @param enforcer Used for permission enforcing + * @param permissionProvider Used to push permissions to audioserver + * @param audioserverLifecycleExecutor Used for tasks managing audioserver lifecycle */ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer, - /* @NonNull */ AudioServerPermissionProvider permissionProvider) { + /* @NonNull */ AudioServerPermissionProvider permissionProvider, + Executor audioserverLifecycleExecutor) { super(enforcer); sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; @@ -1159,6 +1165,7 @@ public class AudioService extends IAudioService.Stub mAppOps = appOps; mPermissionProvider = permissionProvider; + mAudioServerLifecycleExecutor = audioserverLifecycleExecutor; mAudioSystem = audioSystem; mSystemServer = systemServer; @@ -1170,6 +1177,34 @@ public class AudioService extends IAudioService.Stub mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast"); mBroadcastHandlerThread.start(); + // Listen to permission invalidations for the PermissionProvider + if (audioserverPermissions()) { + final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler(); + mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO, + new Runnable() { + // Roughly chosen to be long enough to suppress the autocork behavior + // of the permission cache (50ms), and longer than the task could reasonably + // take, even with many packages and users, while not introducing visible + // permission leaks - since the app needs to restart, and trigger an action + // which requires permissions from audioserver before this delay. + // For RECORD_AUDIO, we are additionally protected by appops. + final long UPDATE_DELAY_MS = 110; + final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0); + @Override + public void run() { + var currentTime = SystemClock.uptimeMillis(); + if (currentTime > scheduledUpdateTimestamp.get()) { + scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS); + broadcastHandler.postAtTime( () -> + mAudioServerLifecycleExecutor.execute(mPermissionProvider + ::onPermissionStateChanged), + currentTime + UPDATE_DELAY_MS + ); + } + } + }); + } + mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); mIsSingleVolume = AudioSystem.isSingleVolume(context); @@ -9918,9 +9953,9 @@ public class AudioService extends IAudioService.Stub int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; synchronized (mCachedAbsVolDrivingStreamsLock) { mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> { - if (stream != null && !mAvrcpAbsVolSupported) { + if (!mAvrcpAbsVolSupported) { mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/ - "", /*enabled*/false, AudioSystem.DEVICE_NONE); + "", /*enabled*/false, AudioSystem.STREAM_DEFAULT); return null; } // For A2DP and AVRCP we need to set the driving stream based on the @@ -11974,29 +12009,6 @@ public class AudioService extends IAudioService.Stub provider.onServiceStart(audioPolicy.getPermissionController()); }); - // Set up event listeners - // Must be kept in sync with PermissionManager - Runnable cacheSysPropHandler = new Runnable() { - private AtomicReference<SystemProperties.Handle> mHandle = new AtomicReference(); - private AtomicLong mNonce = new AtomicLong(); - @Override - public void run() { - if (mHandle.get() == null) { - // Cache the handle - mHandle.compareAndSet(null, SystemProperties.find( - PermissionManager.CACHE_KEY_PACKAGE_INFO)); - } - long nonce; - SystemProperties.Handle ref; - if ((ref = mHandle.get()) != null && (nonce = ref.getLong(0)) != 0 && - mNonce.getAndSet(nonce) != nonce) { - audioserverExecutor.execute(() -> provider.onPermissionStateChanged()); - } - } - }; - - SystemProperties.addChangeCallback(cacheSysPropHandler); - IntentFilter packageUpdateFilter = new IntentFilter(); packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7f4bc74bd59e..d083c68c4c2c 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -748,6 +748,10 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, return AudioSystem.setMasterMute(mute); } + public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) { + AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback); + } + /** * Part of AudioService dump * @param pw diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 8e8a0378c111..8ec835bae0bf 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -21,6 +21,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_NO_AUTHENTICATION; +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; @@ -41,6 +42,7 @@ import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; @@ -54,8 +56,12 @@ import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorPropertiesInternal; import android.hardware.camera2.CameraManager; +import android.hardware.face.FaceManager; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.security.keymint.HardwareAuthenticatorType; import android.net.Uri; import android.os.Binder; @@ -234,6 +240,8 @@ public class BiometricService extends SystemService { private static final boolean DEFAULT_APP_ENABLED = true; private static final boolean DEFAULT_ALWAYS_REQUIRE_CONFIRMATION = false; private static final boolean DEFAULT_MANDATORY_BIOMETRICS_STATUS = false; + private static final boolean DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS = + true; // Some devices that shipped before S already have face-specific settings. Instead of // migrating, which is complicated, let's just keep using the existing settings. @@ -256,6 +264,8 @@ public class BiometricService extends SystemService { Settings.Secure.getUriFor(Settings.Secure.BIOMETRIC_APP_ENABLED); private final Uri MANDATORY_BIOMETRICS_ENABLED = Settings.Secure.getUriFor(Settings.Secure.MANDATORY_BIOMETRICS); + private final Uri MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED = Settings.Secure.getUriFor( + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED); private final ContentResolver mContentResolver; private final List<BiometricService.EnabledOnKeyguardCallback> mCallbacks; @@ -264,6 +274,12 @@ public class BiometricService extends SystemService { private final Map<Integer, Boolean> mBiometricEnabledForApps = new HashMap<>(); private final Map<Integer, Boolean> mFaceAlwaysRequireConfirmation = new HashMap<>(); private final Map<Integer, Boolean> mMandatoryBiometricsEnabled = new HashMap<>(); + private final Map<Integer, Boolean> mMandatoryBiometricsRequirementsSatisfied = + new HashMap<>(); + private final Map<Integer, Boolean> mFingerprintEnrolledForUser = + new HashMap<>(); + private final Map<Integer, Boolean> mFaceEnrolledForUser = + new HashMap<>(); /** * Creates a content observer. @@ -288,7 +304,13 @@ public class BiometricService extends SystemService { mMandatoryBiometricsEnabled.put(context.getUserId(), Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS, DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0, context.getUserId()) != 0); + mMandatoryBiometricsRequirementsSatisfied.put(context.getUserId(), + Settings.Secure.getIntForUser(mContentResolver, + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, + DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS ? 1 : 0, + context.getUserId()) != 0); + addBiometricListenersForMandatoryBiometrics(context); updateContentObserver(); } @@ -322,6 +344,10 @@ public class BiometricService extends SystemService { false /* notifyForDescendants */, this /* observer */, UserHandle.USER_ALL); + mContentResolver.registerContentObserver(MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, + false /* notifyForDescendants */, + this /* observer */, + UserHandle.USER_ALL); } @Override @@ -370,6 +396,13 @@ public class BiometricService extends SystemService { Settings.Secure.MANDATORY_BIOMETRICS, DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0 /* default */, userId) != 0); + } else if (MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED.equals(uri)) { + mMandatoryBiometricsRequirementsSatisfied.put(userId, Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, + DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS + ? 1 : 0 /* default */, + userId) != 0); } } @@ -411,9 +444,15 @@ public class BiometricService extends SystemService { } } - public boolean getMandatoryBiometricsEnabledForUser(int userId) { + public boolean getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(int userId) { return mMandatoryBiometricsEnabled.getOrDefault(userId, - DEFAULT_MANDATORY_BIOMETRICS_STATUS); + DEFAULT_MANDATORY_BIOMETRICS_STATUS) + && mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId, + DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS) + && mBiometricEnabledForApps.getOrDefault(userId, DEFAULT_APP_ENABLED) + && getEnabledForApps(userId) + && (mFingerprintEnrolledForUser.getOrDefault(userId, false /* default */) + || mFaceEnrolledForUser.getOrDefault(userId, false /* default */)); } void notifyEnabledOnKeyguardCallbacks(int userId) { @@ -424,6 +463,79 @@ public class BiometricService extends SystemService { userId); } } + + private void addBiometricListenersForMandatoryBiometrics(Context context) { + final FingerprintManager fingerprintManager = context.getSystemService( + FingerprintManager.class); + final FaceManager faceManager = context.getSystemService(FaceManager.class); + if (fingerprintManager != null) { + fingerprintManager.addAuthenticatorsRegisteredCallback( + new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> list) { + if (list == null || list.isEmpty()) { + Slog.d(TAG, "No fingerprint authenticators registered."); + return; + } + final FingerprintSensorPropertiesInternal + fingerprintSensorProperties = list.get(0); + if (fingerprintSensorProperties.sensorStrength + == STRENGTH_STRONG) { + fingerprintManager.registerBiometricStateListener( + new BiometricStateListener() { + @Override + public void onEnrollmentsChanged( + int userId, + int sensorId, + boolean hasEnrollments + ) { + if (sensorId == fingerprintSensorProperties + .sensorId) { + mFingerprintEnrolledForUser.put(userId, + hasEnrollments); + } + } + }); + } + } + }); + } + if (faceManager != null) { + faceManager.addAuthenticatorsRegisteredCallback( + new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FaceSensorPropertiesInternal> list) { + if (list == null || list.isEmpty()) { + Slog.d(TAG, "No face authenticators registered."); + return; + } + final FaceSensorPropertiesInternal + faceSensorPropertiesInternal = list.get(0); + if (faceSensorPropertiesInternal.sensorStrength + == STRENGTH_STRONG) { + faceManager.registerBiometricStateListener( + new BiometricStateListener() { + @Override + public void onEnrollmentsChanged( + int userId, + int sensorId, + boolean hasEnrollments + ) { + if (sensorId + == faceSensorPropertiesInternal + .sensorId) { + mFaceEnrolledForUser.put(userId, + hasEnrollments); + } + } + }); + } + } + }); + } + } } final class EnabledOnKeyguardCallback implements IBinder.DeathRecipient { diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index b9e6563a7d6a..0bd22f3da67f 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -112,8 +112,8 @@ class PreAuthInfo { == BiometricManager.Authenticators.MANDATORY_BIOMETRICS; if (dropCredentialFallback(promptInfo.getAuthenticators(), - settingObserver.getMandatoryBiometricsEnabledForUser(userId), - trustManager)) { + settingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser( + userId), trustManager)) { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setNegativeButtonText(context.getString(R.string.cancel)); } diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 73aa14ba016f..78f71877afed 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -684,7 +684,8 @@ public class ClipboardService extends SystemService { if (clipboard == null) { return null; } - showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard); + showAccessNotificationLocked( + pkg, intendingUid, intendingUserId, clipboard, deviceId); notifyTextClassifierLocked(clipboard, pkg, intendingUid); if (clipboard.primaryClip != null) { scheduleAutoClear(userId, intendingUid, intendingDeviceId); @@ -1438,7 +1439,7 @@ public class ClipboardService extends SystemService { */ @GuardedBy("mLock") private void showAccessNotificationLocked(String callingPackage, int uid, @UserIdInt int userId, - Clipboard clipboard) { + Clipboard clipboard, int accessDeviceId) { if (clipboard.primaryClip == null) { return; } @@ -1477,7 +1478,7 @@ public class ClipboardService extends SystemService { return; } - final ArraySet<Context> toastContexts = getToastContexts(clipboard); + final ArraySet<Context> toastContexts = getToastContexts(clipboard, accessDeviceId); Binder.withCleanCallingIdentity(() -> { try { CharSequence callingAppLabel = mPm.getApplicationLabel( @@ -1516,40 +1517,55 @@ public class ClipboardService extends SystemService { * If the clipboard is for a VirtualDevice, we attempt to return the single DisplayContext for * the focused VirtualDisplay for that device, but might need to return the contexts for * multiple displays if the VirtualDevice has several but none of them were focused. + * + * If the clipboard is NOT for a VirtualDevice, but it's being accessed from a VirtualDevice, + * this means that the clipboard is shared between the default and that device. In this case we + * need to show a toast in both places. */ - private ArraySet<Context> getToastContexts(Clipboard clipboard) throws IllegalStateException { + private ArraySet<Context> getToastContexts(Clipboard clipboard, int accessDeviceId) + throws IllegalStateException { ArraySet<Context> contexts = new ArraySet<>(); + if (clipboard.deviceId == DEVICE_ID_DEFAULT || accessDeviceId == DEVICE_ID_DEFAULT) { + // Always show the toast on the default display when the default clipboard is accessed - + // also when the clipboard is shared with a virtual device and accessed from there. + // Same when any clipboard is accessed from the default device. + contexts.add(getContext()); + } - if (mVdmInternal != null && clipboard.deviceId != DEVICE_ID_DEFAULT) { - DisplayManager displayManager = getContext().getSystemService(DisplayManager.class); + if ((accessDeviceId == DEVICE_ID_DEFAULT && clipboard.deviceId == DEVICE_ID_DEFAULT) + || mVdmInternal == null) { + // No virtual devices involved. + return contexts; + } - int topFocusedDisplayId = mWm.getTopFocusedDisplayId(); - ArraySet<Integer> displayIds = mVdmInternal.getDisplayIdsForDevice(clipboard.deviceId); + // At this point the clipboard is either accessed from a virtual device, or it is a virtual + // device clipboard, so show a toast on the relevant virtual display(s). + DisplayManager displayManager = getContext().getSystemService(DisplayManager.class); + ArraySet<Integer> displayIds = mVdmInternal.getDisplayIdsForDevice(accessDeviceId); + int topFocusedDisplayId = mWm.getTopFocusedDisplayId(); - if (displayIds.contains(topFocusedDisplayId)) { - Display display = displayManager.getDisplay(topFocusedDisplayId); - if (display != null) { - contexts.add(getContext().createDisplayContext(display)); - return contexts; - } + if (displayIds.contains(topFocusedDisplayId)) { + Display display = displayManager.getDisplay(topFocusedDisplayId); + if (display != null) { + contexts.add(getContext().createDisplayContext(display)); + return contexts; } + } - for (int i = 0; i < displayIds.size(); i++) { - Display display = displayManager.getDisplay(displayIds.valueAt(i)); - if (display != null) { - contexts.add(getContext().createDisplayContext(display)); - } - } - if (!contexts.isEmpty()) { - return contexts; + for (int i = 0; i < displayIds.size(); i++) { + Display display = displayManager.getDisplay(displayIds.valueAt(i)); + if (display != null) { + contexts.add(getContext().createDisplayContext(display)); } + } + if (contexts.isEmpty()) { Slog.e(TAG, "getToastContexts Couldn't find any VirtualDisplays for VirtualDevice " - + clipboard.deviceId); + + accessDeviceId); // Since we couldn't find any VirtualDisplays to use at all, just fall through to using // the default display below. + contexts.add(getContext()); } - contexts.add(getContext()); return contexts; } diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java index 22b85d45d15e..a9fe8cb01b3a 100644 --- a/services/core/java/com/android/server/compat/PlatformCompat.java +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -248,6 +248,25 @@ public class PlatformCompat extends IPlatformCompat.Stub { return enabled; } + /** + * Internal version of {@link #isChangeEnabledByUid(long, int)}. + * + * <p>Does not perform costly permission check and logging. + */ + public boolean isChangeEnabledByUidInternalNoLogging(long changeId, int uid) { + String[] packages = mContext.getPackageManager().getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return mCompatConfig.defaultChangeIdValue(changeId); + } + boolean enabled = true; + final int userId = UserHandle.getUserId(uid); + for (String packageName : packages) { + final var appInfo = getApplicationInfo(packageName, userId); + enabled &= isChangeEnabledInternalNoLogging(changeId, appInfo); + } + return enabled; + } + @Override @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG) public void setOverrides(CompatibilityChangeConfig overrides, String packageName) { diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java index 133c79f81bb5..8e725465ddd6 100644 --- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java +++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java @@ -24,6 +24,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.net.ConnectivityModuleConnector; +import android.sysprop.CrashRecoveryProperties; import android.text.TextUtils; import android.util.Slog; @@ -35,7 +36,7 @@ import java.util.List; /** * Provides helper methods for the CrashRecovery APEX - * + * TODO: b/354112511 Add tests for this class when it is finalized. * @hide */ public final class CrashRecoveryHelper { @@ -76,11 +77,13 @@ public final class CrashRecoveryHelper { } /** - * Register health listeners for explicit package failures. - * Currently only registering for Connectivity Module health. - * @hide + * Register health listeners for Connectivity packages health. + * + * TODO: b/354112511 Have an internal method to trigger a rollback by reporting high severity errors, + * and rely on ActivityManager to inform the watchdog of severe network stack crashes + * instead of having this listener in parallel. */ - public void registerConnectivityModuleHealthListener(@NonNull int failureReason) { + public void registerConnectivityModuleHealthListener() { // register listener for ConnectivityModule mConnectivityModuleConnector.registerHealthListener( packageName -> { @@ -90,7 +93,7 @@ public final class CrashRecoveryHelper { return; } final List<VersionedPackage> pkgList = Collections.singletonList(pkg); - PackageWatchdog.getInstance(mContext).onPackageFailure(pkgList, failureReason); + PackageWatchdog.getInstance(mContext).onPackageFailure(pkgList, PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK); }); } @@ -126,4 +129,21 @@ public final class CrashRecoveryHelper { return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); } } + + /** + * Check if we're currently attempting to reboot for a factory reset. This method must + * return true if RescueParty tries to reboot early during a boot loop, since the device + * will not be fully booted at this time. + */ + public static boolean isRecoveryTriggeredReboot() { + return isFactoryResetPropertySet() || isRebootPropertySet(); + } + + static boolean isFactoryResetPropertySet() { + return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); + } + + static boolean isRebootPropertySet() { + return CrashRecoveryProperties.attemptingReboot().orElse(false); + } } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 7a055d1d7e5c..ed6ed60a6806 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -99,6 +99,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import javax.xml.datatype.DatatypeConfigurationException; @@ -1192,6 +1193,18 @@ public class DisplayDeviceConfig { */ public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) { Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null; + return getHdrBrightnessFromSdr(brightness, maxDesiredHdrSdrRatio, sdrToHdrSpline); + } + + /** + * Calculate the HDR brightness for the specified SDR brightenss, restricted by the + * maxDesiredHdrSdrRatio (the ratio between the HDR luminance and SDR luminance) and specific + * sdrToHdrSpline + * + * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists. + */ + public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio, + @Nullable Spline sdrToHdrSpline) { if (sdrToHdrSpline == null) { return PowerManager.BRIGHTNESS_INVALID; } @@ -1786,15 +1799,17 @@ public class DisplayDeviceConfig { loadThermalThrottlingConfig(config); loadPowerThrottlingConfigData(config); // Backlight and evenDimmer data should be loaded for HbmData - mHbmData = HighBrightnessModeData.loadHighBrightnessModeData(config, (hbm) -> { + Function<HighBrightnessMode, Float> transitionPointProvider = (hbm) -> { float transitionPointBacklightScale = hbm.getTransitionPoint_all().floatValue(); if (transitionPointBacklightScale >= mBacklightMaximum) { throw new IllegalArgumentException("HBM transition point invalid. " - + mHbmData.transitionPoint + " is not less than " + + transitionPointBacklightScale + " is not less than " + mBacklightMaximum); } return getBrightnessFromBacklight(transitionPointBacklightScale); - }); + }; + mHbmData = HighBrightnessModeData.loadHighBrightnessModeData(config, + transitionPointProvider); if (mHbmData.isHighBrightnessModeEnabled && mHbmData.refreshRateLimit != null) { // TODO(b/331650248): cleanup, DMD can use mHbmData.refreshRateLimit mRefreshRateLimitations.add(new RefreshRateLimitation( @@ -1818,7 +1833,7 @@ public class DisplayDeviceConfig { loadRefreshRateSetting(config); loadScreenOffBrightnessSensorValueToLuxFromDdc(config); loadUsiVersion(config); - mHdrBrightnessData = HdrBrightnessData.loadConfig(config); + mHdrBrightnessData = HdrBrightnessData.loadConfig(config, transitionPointProvider); loadBrightnessCapForWearBedtimeMode(config); loadIdleScreenRefreshRateTimeoutConfigs(config); mVrrSupportEnabled = config.getSupportsVrr(); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b3a6c1c1e20a..2cec869c290e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3849,9 +3849,10 @@ public final class DisplayManagerService extends SystemService { // Ignore redundant events. Further optimization is possible by merging adjacent events. Pair<Integer, Integer> last = mDisplayEvents.get(mDisplayEvents.size() - 1); if (last.first == displayId && last.second == event) { - Slog.d(TAG, - "Ignore redundant display event " + displayId + "/" + event + " to " - + mCallbackRecord.mUid + "/" + mCallbackRecord.mPid); + if (DEBUG) { + Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event + " to " + + mCallbackRecord.mUid + "/" + mCallbackRecord.mPid); + } return; } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 6992580e4df8..1177be212222 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -505,14 +505,15 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mClock = mInjector.getClock(); mLogicalDisplay = logicalDisplay; mDisplayId = mLogicalDisplay.getDisplayIdLocked(); + mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + IBinder displayToken = mDisplayDevice.getDisplayTokenLocked(); + DisplayDeviceInfo displayDeviceInfo = mDisplayDevice.getDisplayDeviceInfoLocked(); mSensorManager = sensorManager; mHandler = new DisplayControllerHandler(handler.getLooper()); - mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked() - .getDisplayDeviceConfig(); + mDisplayDeviceConfig = mDisplayDevice.getDisplayDeviceConfig(); mIsEnabled = logicalDisplay.isEnabledLocked(); mIsInTransition = logicalDisplay.isInTransitionLocked(); - mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked() - .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL; + mIsDisplayInternal = displayDeviceInfo.type == Display.TYPE_INTERNAL; mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks); mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController( mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(), @@ -521,7 +522,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mTag = TAG + "[" + mDisplayId + "]"; mThermalBrightnessThrottlingDataId = logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId; - mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + mUniqueDisplayId = mDisplayDevice.getUniqueId(); mDisplayStatsId = mUniqueDisplayId.hashCode(); mPhysicalDisplayName = mDisplayDevice.getNameLocked(); @@ -569,8 +570,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController, modeChangeCallback, mDisplayDeviceConfig, mHandler, flags, - mDisplayDevice.getDisplayTokenLocked(), - mDisplayDevice.getDisplayDeviceInfoLocked()); + displayToken, displayDeviceInfo); mDisplayBrightnessController = new DisplayBrightnessController(context, null, @@ -584,8 +584,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mUniqueDisplayId, mThermalBrightnessThrottlingDataId, logicalDisplay.getPowerThrottlingDataIdLocked(), - mDisplayDeviceConfig, - mDisplayId), mContext, flags, mSensorManager); + mDisplayDeviceConfig, displayDeviceInfo.width, displayDeviceInfo.height, + displayToken, mDisplayId), mContext, flags, mSensorManager); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); mAutomaticBrightnessStrategy = @@ -893,7 +893,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessClamperController.onDisplayChanged( new BrightnessClamperController.DisplayDeviceData(uniqueId, thermalBrightnessThrottlingDataId, powerThrottlingDataId, - config, mDisplayId)); + config, info.width, info.height, token, mDisplayId)); if (changed) { updatePowerState(); diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index 88d2c007cf37..9324fc1c4e06 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -28,13 +28,16 @@ import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.IBinder; import android.os.PowerManager; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; import android.util.IndentingPrintWriter; import android.util.Slog; +import android.util.Spline; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData; @@ -65,6 +68,11 @@ public class BrightnessClamperController { private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers; private final List<BrightnessStateModifier> mModifiers; + + private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>(); + private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>(); + private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState(); + private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; @@ -110,7 +118,16 @@ public class BrightnessClamperController { mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags, context); mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener, - data.mDisplayDeviceConfig); + data); + + mModifiers.forEach(m -> { + if (m instanceof DisplayDeviceDataListener l) { + mDisplayDeviceDataListeners.add(l); + } + if (m instanceof StatefulModifier s) { + mStatefulModifiers.add(s); + } + }); mOnPropertiesChangedListener = properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId()); @@ -123,6 +140,7 @@ public class BrightnessClamperController { public void onDisplayChanged(DisplayDeviceData data) { mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId()); mClampers.forEach(clamper -> clamper.onDisplayChanged(data)); + mDisplayDeviceDataListeners.forEach(l -> l.onDisplayChanged(data)); adjustLightSensorSubscription(); } @@ -234,14 +252,28 @@ public class BrightnessClamperController { customAnimationRate = minClamper.getCustomAnimationRate(); } + ModifiersAggregatedState newAggregatedState = new ModifiersAggregatedState(); + mStatefulModifiers.forEach((clamper) -> clamper.applyStateChange(newAggregatedState)); + if (mBrightnessCap != brightnessCap || mClamperType != clamperType - || mCustomAnimationRate != customAnimationRate) { + || mCustomAnimationRate != customAnimationRate + || needToNotifyExternalListener(mModifiersAggregatedState, newAggregatedState)) { mBrightnessCap = brightnessCap; mClamperType = clamperType; mCustomAnimationRate = customAnimationRate; mClamperChangeListenerExternal.onChanged(); } + mModifiersAggregatedState = newAggregatedState; + } + + private boolean needToNotifyExternalListener(ModifiersAggregatedState state1, + ModifiersAggregatedState state2) { + return !BrightnessSynchronizer.floatEquals(state1.mMaxDesiredHdrRatio, + state2.mMaxDesiredHdrRatio) + || !BrightnessSynchronizer.floatEquals(state1.mMaxHdrBrightness, + state2.mMaxHdrBrightness) + || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline; } private void start() { @@ -295,17 +327,16 @@ public class BrightnessClamperController { List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, Handler handler, ClamperChangeListener listener, - DisplayDeviceConfig displayDeviceConfig) { + DisplayDeviceData data) { List<BrightnessStateModifier> modifiers = new ArrayList<>(); modifiers.add(new DisplayDimModifier(context)); modifiers.add(new BrightnessLowPowerModeModifier()); - if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null - && displayDeviceConfig.isEvenDimmerAvailable()) { + if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) { modifiers.add(new BrightnessLowLuxModifier(handler, listener, context, - displayDeviceConfig)); + data.mDisplayDeviceConfig)); } if (flags.useNewHdrBrightnessModifier()) { - modifiers.add(new HdrBrightnessModifier()); + modifiers.add(new HdrBrightnessModifier(handler, listener, data)); } return modifiers; } @@ -319,7 +350,14 @@ public class BrightnessClamperController { } /** - * Config Data for clampers + * Modifier should implement this interface in order to receive display change updates + */ + interface DisplayDeviceDataListener { + void onDisplayChanged(DisplayDeviceData displayData); + } + + /** + * Config Data for clampers/modifiers */ public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData, BrightnessPowerClamper.PowerData, @@ -331,23 +369,34 @@ public class BrightnessClamperController { @NonNull private final String mPowerThrottlingDataId; @NonNull - private final DisplayDeviceConfig mDisplayDeviceConfig; + final DisplayDeviceConfig mDisplayDeviceConfig; + + final int mWidth; - private final int mDisplayId; + final int mHeight; + + final IBinder mDisplayToken; + + final int mDisplayId; public DisplayDeviceData(@NonNull String uniqueDisplayId, @NonNull String thermalThrottlingDataId, @NonNull String powerThrottlingDataId, @NonNull DisplayDeviceConfig displayDeviceConfig, + int width, + int height, + IBinder displayToken, int displayId) { mUniqueDisplayId = uniqueDisplayId; mThermalThrottlingDataId = thermalThrottlingDataId; mPowerThrottlingDataId = powerThrottlingDataId; mDisplayDeviceConfig = displayDeviceConfig; + mWidth = width; + mHeight = height; + mDisplayToken = displayToken; mDisplayId = displayId; } - @NonNull @Override public String getUniqueDisplayId() { @@ -406,4 +455,24 @@ public class BrightnessClamperController { return mDisplayId; } } + + /** + * Stateful modifier should implement this interface and modify aggregatedState. + * AggregatedState is used by Controller to determine if updatePowerState call is needed + * to correctly adjust brightness + */ + interface StatefulModifier { + void applyStateChange(ModifiersAggregatedState aggregatedState); + } + + /** + * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness + * adjustement is needed + */ + public static class ModifiersAggregatedState { + float mMaxDesiredHdrRatio = HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO; + float mMaxHdrBrightness = PowerManager.BRIGHTNESS_MAX; + @Nullable + Spline mSdrHdrRatioSpline = null; + } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java index a829866ae6fc..5e44cc357b28 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java @@ -16,36 +16,325 @@ package com.android.server.display.brightness.clamper; +import static com.android.server.display.DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; +import static com.android.server.display.brightness.clamper.LightSensorController.INVALID_LUX; + +import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.hardware.display.DisplayManagerInternal; +import android.os.Handler; +import android.os.IBinder; +import android.os.PowerManager; +import android.view.SurfaceControlHdrLayerInfoListener; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.config.HdrBrightnessData; import java.io.PrintWriter; +import java.util.Map; + +public class HdrBrightnessModifier implements BrightnessStateModifier, + BrightnessClamperController.DisplayDeviceDataListener, + BrightnessClamperController.StatefulModifier { + + static final float DEFAULT_MAX_HDR_SDR_RATIO = 1.0f; + private static final float DEFAULT_HDR_LAYER_SIZE = -1.0f; + + private final SurfaceControlHdrLayerInfoListener mHdrListener = + new SurfaceControlHdrLayerInfoListener() { + @Override + public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, int maxW, + int maxH, int flags, float maxDesiredHdrSdrRatio) { + boolean hdrLayerPresent = numberOfHdrLayers > 0; + mHandler.post(() -> HdrBrightnessModifier.this.onHdrInfoChanged( + hdrLayerPresent ? (float) (maxW * maxH) : DEFAULT_HDR_LAYER_SIZE, + hdrLayerPresent ? maxDesiredHdrSdrRatio : DEFAULT_MAX_HDR_SDR_RATIO)); + } + }; + + private final Handler mHandler; + private final BrightnessClamperController.ClamperChangeListener mClamperChangeListener; + private final Injector mInjector; + private final Runnable mDebouncer; + + private IBinder mRegisteredDisplayToken; + + private DisplayDeviceConfig mDisplayDeviceConfig; + @Nullable + private HdrBrightnessData mHdrBrightnessData; + private float mScreenSize; -public class HdrBrightnessModifier implements BrightnessStateModifier { + private float mMaxDesiredHdrRatio = DEFAULT_MAX_HDR_SDR_RATIO; + private float mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE; + + private float mAmbientLux = INVALID_LUX; + + private Mode mMode = Mode.NO_HDR; + // The maximum brightness allowed for current lux + private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX; + private float mPendingMaxBrightness = PowerManager.BRIGHTNESS_MAX; + // brightness change speed, in units per seconds. Applied only on ambient lux changes + private float mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET; + private float mPendingTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET; + + HdrBrightnessModifier(Handler handler, + BrightnessClamperController.ClamperChangeListener clamperChangeListener, + BrightnessClamperController.DisplayDeviceData displayData) { + this(new Handler(handler.getLooper()), clamperChangeListener, new Injector(), displayData); + } + + @VisibleForTesting + HdrBrightnessModifier(Handler handler, + BrightnessClamperController.ClamperChangeListener clamperChangeListener, + Injector injector, + BrightnessClamperController.DisplayDeviceData displayData) { + mHandler = handler; + mClamperChangeListener = clamperChangeListener; + mInjector = injector; + mDebouncer = () -> { + mTransitionRate = mPendingTransitionRate; + mMaxBrightness = mPendingMaxBrightness; + mClamperChangeListener.onChanged(); + }; + onDisplayChanged(displayData); + } + + // Called in DisplayControllerHandler @Override public void apply(DisplayManagerInternal.DisplayPowerRequest request, DisplayBrightnessState.Builder stateBuilder) { - // noop + if (mHdrBrightnessData == null) { // no hdr data + return; + } + if (mMode == Mode.NO_HDR) { + return; + } + float hdrBrightness = mDisplayDeviceConfig.getHdrBrightnessFromSdr( + stateBuilder.getBrightness(), mMaxDesiredHdrRatio, + mHdrBrightnessData.sdrToHdrRatioSpline); + float maxBrightness = getMaxBrightness(mMode, mMaxBrightness, mHdrBrightnessData); + hdrBrightness = Math.min(hdrBrightness, maxBrightness); + + stateBuilder.setHdrBrightness(hdrBrightness); + stateBuilder.setCustomAnimationRate(mTransitionRate); + // transition rate applied, reset + mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET; } @Override - public void dump(PrintWriter printWriter) { - // noop + public void dump(PrintWriter pw) { + pw.println("HdrBrightnessModifier:"); + pw.println(" mHdrBrightnessData=" + mHdrBrightnessData); + pw.println(" mScreenSize=" + mScreenSize); + pw.println(" mMaxDesiredHdrRatio=" + mMaxDesiredHdrRatio); + pw.println(" mHdrLayerSize=" + mHdrLayerSize); + pw.println(" mAmbientLux=" + mAmbientLux); + pw.println(" mMode=" + mMode); + pw.println(" mMaxBrightness=" + mMaxBrightness); + pw.println(" mPendingMaxBrightness=" + mPendingMaxBrightness); + pw.println(" mTransitionRate=" + mTransitionRate); + pw.println(" mPendingTransitionRate=" + mPendingTransitionRate); + pw.println(" mHdrListener registered=" + (mRegisteredDisplayToken != null)); } + // Called in DisplayControllerHandler @Override public void stop() { - // noop + unregisterHdrListener(); + mHandler.removeCallbacksAndMessages(null); } + // Called in DisplayControllerHandler @Override public boolean shouldListenToLightSensor() { - return false; + return hasBrightnessLimits(); } + // Called in DisplayControllerHandler @Override public void setAmbientLux(float lux) { - // noop + mAmbientLux = lux; + if (!hasBrightnessLimits()) { + return; + } + float desiredMaxBrightness = findBrightnessLimit(mHdrBrightnessData, lux); + if (mMode == Mode.NO_HDR) { + mMaxBrightness = desiredMaxBrightness; + } else { + scheduleMaxBrightnessUpdate(desiredMaxBrightness, mHdrBrightnessData); + } + } + + @Override + public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) { + mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth, + displayData.mHeight, displayData.mDisplayDeviceConfig)); + } + + // Called in DisplayControllerHandler, when any modifier state changes + @Override + public void applyStateChange( + BrightnessClamperController.ModifiersAggregatedState aggregatedState) { + if (mMode != Mode.NO_HDR && mHdrBrightnessData != null) { + aggregatedState.mMaxDesiredHdrRatio = mMaxDesiredHdrRatio; + aggregatedState.mSdrHdrRatioSpline = mHdrBrightnessData.sdrToHdrRatioSpline; + aggregatedState.mMaxHdrBrightness = getMaxBrightness( + mMode, mMaxBrightness, mHdrBrightnessData); + } + } + + private boolean hasBrightnessLimits() { + return mHdrBrightnessData != null && !mHdrBrightnessData.maxBrightnessLimits.isEmpty(); + } + + private void scheduleMaxBrightnessUpdate(float desiredMaxBrightness, HdrBrightnessData data) { + if (mMaxBrightness == desiredMaxBrightness) { + mPendingMaxBrightness = mMaxBrightness; + mPendingTransitionRate = -1f; + mTransitionRate = -1f; + mHandler.removeCallbacks(mDebouncer); + } else if (mPendingMaxBrightness != desiredMaxBrightness) { + mPendingMaxBrightness = desiredMaxBrightness; + long debounceTime; + if (mPendingMaxBrightness > mMaxBrightness) { + debounceTime = data.brightnessIncreaseDebounceMillis; + mPendingTransitionRate = data.screenBrightnessRampIncrease; + } else { + debounceTime = data.brightnessDecreaseDebounceMillis; + mPendingTransitionRate = data.screenBrightnessRampDecrease; + } + + mHandler.removeCallbacks(mDebouncer); + mHandler.postDelayed(mDebouncer, debounceTime); + } + // do nothing if expectedMaxBrightness == mDesiredMaxBrightness + // && expectedMaxBrightness != mMaxBrightness + } + + // Called in DisplayControllerHandler + private void onDisplayChanged(IBinder displayToken, int width, int height, + DisplayDeviceConfig config) { + mDisplayDeviceConfig = config; + mScreenSize = (float) width * height; + HdrBrightnessData data = config.getHdrBrightnessData(); + if (data == null) { + unregisterHdrListener(); + } else { + registerHdrListener(displayToken); + } + recalculate(data, mMaxDesiredHdrRatio); + } + + // Called in DisplayControllerHandler + private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) { + Mode newMode = recalculateMode(data); + // if HDR mode changed, notify changed + boolean needToNotifyChange = mMode != newMode; + // If HDR mode is active, we need to check if other HDR params are changed + if (mMode != HdrBrightnessModifier.Mode.NO_HDR) { + if (!BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrRatio) + || data != mHdrBrightnessData) { + needToNotifyChange = true; + } + } + + mMode = newMode; + mHdrBrightnessData = data; + mMaxDesiredHdrRatio = maxDesiredHdrRatio; + + if (needToNotifyChange) { + // data or hdr layer changed, reset custom transition rate + mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET; + mClamperChangeListener.onChanged(); + } + } + + // Called in DisplayControllerHandler + private Mode recalculateMode(@Nullable HdrBrightnessData data) { + // no config + if (data == null) { + return Mode.NO_HDR; + } + // HDR layer < minHdr % for Nbm + if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) { + return Mode.NO_HDR; + } + // HDR layer < minHdr % for Hbm, and HDR layer >= that minHdr % for Nbm + if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForHbm) { + return Mode.NBM_HDR; + } + // HDR layer > that minHdr % for Hbm + return Mode.HBM_HDR; + } + + private float getMaxBrightness(Mode mode, float maxBrightness, HdrBrightnessData data) { + if (mode == Mode.NBM_HDR) { + return Math.min(data.hbmTransitionPoint, maxBrightness); + } else if (mode == Mode.HBM_HDR) { + return maxBrightness; + } else { + return PowerManager.BRIGHTNESS_MAX; + } + } + + // Called in DisplayControllerHandler + private float findBrightnessLimit(HdrBrightnessData data, float ambientLux) { + float foundAmbientBoundary = Float.MAX_VALUE; + float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX; + for (Map.Entry<Float, Float> brightnessPoint : + data.maxBrightnessLimits.entrySet()) { + float ambientBoundary = brightnessPoint.getKey(); + // find ambient lux upper boundary closest to current ambient lux + if (ambientBoundary > ambientLux && ambientBoundary < foundAmbientBoundary) { + foundMaxBrightness = brightnessPoint.getValue(); + foundAmbientBoundary = ambientBoundary; + } + } + return foundMaxBrightness; + } + + // Called in DisplayControllerHandler + private void onHdrInfoChanged(float hdrLayerSize, float maxDesiredHdrSdrRatio) { + mHdrLayerSize = hdrLayerSize; + recalculate(mHdrBrightnessData, maxDesiredHdrSdrRatio); + } + + // Called in DisplayControllerHandler + private void registerHdrListener(IBinder displayToken) { + if (mRegisteredDisplayToken == displayToken) { + return; + } + unregisterHdrListener(); + if (displayToken != null) { + mInjector.registerHdrListener(mHdrListener, displayToken); + mRegisteredDisplayToken = displayToken; + } + } + + // Called in DisplayControllerHandler + private void unregisterHdrListener() { + if (mRegisteredDisplayToken != null) { + mInjector.unregisterHdrListener(mHdrListener, mRegisteredDisplayToken); + mRegisteredDisplayToken = null; + mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE; + } + } + + private enum Mode { + NO_HDR, NBM_HDR, HBM_HDR + } + + @SuppressLint("MissingPermission") + static class Injector { + void registerHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) { + listener.register(token); + } + + void unregisterHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) { + listener.unregister(token); + } } } diff --git a/services/core/java/com/android/server/display/config/HdrBrightnessData.java b/services/core/java/com/android/server/display/config/HdrBrightnessData.java index c9408077e5af..ef4a7984755d 100644 --- a/services/core/java/com/android/server/display/config/HdrBrightnessData.java +++ b/services/core/java/com/android/server/display/config/HdrBrightnessData.java @@ -19,6 +19,7 @@ package com.android.server.display.config; import static com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; import android.annotation.Nullable; +import android.os.PowerManager; import android.util.Spline; import com.android.internal.annotations.VisibleForTesting; @@ -29,6 +30,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; /** * Brightness config for HDR content @@ -48,9 +50,9 @@ import java.util.Map; * </point> * </brightnessMap> * <brightnessIncreaseDebounceMillis>1000</brightnessIncreaseDebounceMillis> - * <brightnessIncreaseDurationMillis>10000</brightnessIncreaseDurationMillis> + * <screenBrightnessRampIncrease>0.04</brightnessIncreaseDurationMillis> * <brightnessDecreaseDebounceMillis>13000</brightnessDecreaseDebounceMillis> - * <brightnessDecreaseDurationMillis>10000</brightnessDecreaseDurationMillis> + * <screenBrightnessRampDecrease>0.03</brightnessDecreaseDurationMillis> * <minimumHdrPercentOfScreenForNbm>0.2</minimumHdrPercentOfScreenForNbm> * <minimumHdrPercentOfScreenForHbm>0.5</minimumHdrPercentOfScreenForHbm> * <allowInLowPowerMode>true</allowInLowPowerMode> @@ -99,6 +101,11 @@ public class HdrBrightnessData { public final float screenBrightnessRampDecrease; /** + * Brightness level at which we transition from normal to high-brightness + */ + public final float hbmTransitionPoint; + + /** * Min Hdr layer size to start hdr brightness boost up to high brightness mode transition point */ public final float minimumHdrPercentOfScreenForNbm; @@ -123,6 +130,7 @@ public class HdrBrightnessData { public HdrBrightnessData(Map<Float, Float> maxBrightnessLimits, long brightnessIncreaseDebounceMillis, float screenBrightnessRampIncrease, long brightnessDecreaseDebounceMillis, float screenBrightnessRampDecrease, + float hbmTransitionPoint, float minimumHdrPercentOfScreenForNbm, float minimumHdrPercentOfScreenForHbm, boolean allowInLowPowerMode, @Nullable Spline sdrToHdrRatioSpline) { this.maxBrightnessLimits = maxBrightnessLimits; @@ -130,6 +138,7 @@ public class HdrBrightnessData { this.screenBrightnessRampIncrease = screenBrightnessRampIncrease; this.brightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis; this.screenBrightnessRampDecrease = screenBrightnessRampDecrease; + this.hbmTransitionPoint = hbmTransitionPoint; this.minimumHdrPercentOfScreenForNbm = minimumHdrPercentOfScreenForNbm; this.minimumHdrPercentOfScreenForHbm = minimumHdrPercentOfScreenForHbm; this.allowInLowPowerMode = allowInLowPowerMode; @@ -144,6 +153,7 @@ public class HdrBrightnessData { + ", mScreenBrightnessRampIncrease: " + screenBrightnessRampIncrease + ", mBrightnessDecreaseDebounceMillis: " + brightnessDecreaseDebounceMillis + ", mScreenBrightnessRampDecrease: " + screenBrightnessRampDecrease + + ", transitionPoint: " + hbmTransitionPoint + ", minimumHdrPercentOfScreenForNbm: " + minimumHdrPercentOfScreenForNbm + ", minimumHdrPercentOfScreenForHbm: " + minimumHdrPercentOfScreenForHbm + ", allowInLowPowerMode: " + allowInLowPowerMode @@ -155,10 +165,12 @@ public class HdrBrightnessData { * Loads HdrBrightnessData from DisplayConfiguration */ @Nullable - public static HdrBrightnessData loadConfig(DisplayConfiguration config) { + public static HdrBrightnessData loadConfig(DisplayConfiguration config, + Function<HighBrightnessMode, Float> transitionPointProvider) { + HighBrightnessMode hbmConfig = config.getHighBrightnessMode(); HdrBrightnessConfig hdrConfig = config.getHdrBrightnessConfig(); if (hdrConfig == null) { - return getFallbackData(config.getHighBrightnessMode()); + return getFallbackData(hbmConfig, transitionPointProvider); } List<NonNegativeFloatToFloatPoint> points = hdrConfig.getBrightnessMap().getPoint(); @@ -169,22 +181,38 @@ public class HdrBrightnessData { float minHdrPercentForHbm = hdrConfig.getMinimumHdrPercentOfScreenForHbm() != null ? hdrConfig.getMinimumHdrPercentOfScreenForHbm().floatValue() - : getFallbackHdrPercent(config.getHighBrightnessMode()); + : getFallbackHdrPercent(hbmConfig); float minHdrPercentForNbm = hdrConfig.getMinimumHdrPercentOfScreenForNbm() != null ? hdrConfig.getMinimumHdrPercentOfScreenForNbm().floatValue() : minHdrPercentForHbm; + if (minHdrPercentForNbm > minHdrPercentForHbm) { + throw new IllegalArgumentException( + "minHdrPercentForHbm should be >= minHdrPercentForNbm"); + } + return new HdrBrightnessData(brightnessLimits, hdrConfig.getBrightnessIncreaseDebounceMillis().longValue(), hdrConfig.getScreenBrightnessRampIncrease().floatValue(), hdrConfig.getBrightnessDecreaseDebounceMillis().longValue(), hdrConfig.getScreenBrightnessRampDecrease().floatValue(), + getTransitionPoint(hbmConfig, transitionPointProvider), minHdrPercentForNbm, minHdrPercentForHbm, hdrConfig.getAllowInLowPowerMode(), getSdrHdrRatioSpline(hdrConfig, config.getHighBrightnessMode())); } + private static float getTransitionPoint(@Nullable HighBrightnessMode hbm, + Function<HighBrightnessMode, Float> transitionPointProvider) { + if (hbm == null) { + return PowerManager.BRIGHTNESS_MAX; + } else { + return transitionPointProvider.apply(hbm); + } + } + @Nullable - private static HdrBrightnessData getFallbackData(HighBrightnessMode hbm) { + private static HdrBrightnessData getFallbackData(@Nullable HighBrightnessMode hbm, + Function<HighBrightnessMode, Float> transitionPointProvider) { if (hbm == null) { return null; } @@ -193,6 +221,7 @@ public class HdrBrightnessData { return new HdrBrightnessData(Collections.emptyMap(), 0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET, 0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET, + getTransitionPoint(hbm, transitionPointProvider), fallbackPercent, fallbackPercent, false, fallbackSpline); } diff --git a/services/core/java/com/android/server/display/config/SensorData.java b/services/core/java/com/android/server/display/config/SensorData.java index 8bfc4a3ad77a..1437c8dba0cd 100644 --- a/services/core/java/com/android/server/display/config/SensorData.java +++ b/services/core/java/com/android/server/display/config/SensorData.java @@ -34,6 +34,8 @@ public class SensorData { public static final String TEMPERATURE_TYPE_DISPLAY = "DISPLAY"; public static final String TEMPERATURE_TYPE_SKIN = "SKIN"; + private static final SensorData UNSPECIFIED_SENSOR_DATA = new SensorData( + /* type= */null, /* name= */ null); @Nullable public final String type; @@ -43,24 +45,14 @@ public class SensorData { public final float maxRefreshRate; public final List<SupportedModeData> supportedModes; - @VisibleForTesting - public SensorData() { - this(/* type= */ null, /* name= */ null); - } - - @VisibleForTesting - public SensorData(String type, String name) { - this(type, name, /* minRefreshRate= */ 0f, /* maxRefreshRate= */ Float.POSITIVE_INFINITY); - } - - @VisibleForTesting - public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate) { - this(type, name, minRefreshRate, maxRefreshRate, /* supportedModes= */ List.of()); + private SensorData(@Nullable String type, @Nullable String name) { + this(type, name, /* minRefreshRate= */ 0f, /* maxRefreshRate= */ Float.POSITIVE_INFINITY, + /* supportedModes= */ List.of()); } @VisibleForTesting - public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate, - List<SupportedModeData> supportedModes) { + SensorData(@Nullable String type, @Nullable String name, + float minRefreshRate, float maxRefreshRate, List<SupportedModeData> supportedModes) { this.type = type; this.name = name; this.minRefreshRate = minRefreshRate; @@ -72,7 +64,7 @@ public class SensorData { * @return True if the sensor matches both the specified name and type, or one if only one * is specified (not-empty). Always returns false if both parameters are null or empty. */ - public boolean matches(String sensorName, String sensorType) { + public boolean matches(@Nullable String sensorName, @Nullable String sensorType) { final boolean isNameSpecified = !TextUtils.isEmpty(sensorName); final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType); return (isNameSpecified || isTypeSpecified) @@ -120,7 +112,7 @@ public class SensorData { if (sensorDetails != null) { return loadSensorData(sensorDetails); } else { - return new SensorData(); + return UNSPECIFIED_SENSOR_DATA; } } @@ -130,13 +122,12 @@ public class SensorData { @Nullable public static SensorData loadProxSensorConfig( DisplayManagerFlags flags, DisplayConfiguration config) { - SensorData DEFAULT_SENSOR = new SensorData(); List<SensorDetails> sensorDetailsList = config.getProxSensor(); if (sensorDetailsList.isEmpty()) { - return DEFAULT_SENSOR; + return UNSPECIFIED_SENSOR_DATA; } - SensorData selectedSensor = DEFAULT_SENSOR; + SensorData selectedSensor = UNSPECIFIED_SENSOR_DATA; // Prioritize flagged sensors. for (SensorDetails sensorDetails : sensorDetailsList) { String flagStr = sensorDetails.getFeatureFlag(); @@ -148,7 +139,7 @@ public class SensorData { } // Check for normal un-flagged sensor if a flagged one wasn't found. - if (DEFAULT_SENSOR == selectedSensor) { + if (UNSPECIFIED_SENSOR_DATA == selectedSensor) { for (SensorDetails sensorDetails : sensorDetailsList) { if (sensorDetails.getFeatureFlag() != null) { continue; @@ -159,7 +150,7 @@ public class SensorData { } // Check if we shouldn't use a sensor at all. - if (DEFAULT_SENSOR != selectedSensor) { + if (UNSPECIFIED_SENSOR_DATA != selectedSensor) { if ("".equals(selectedSensor.name) && "".equals(selectedSensor.type)) { // <proxSensor> with empty values to the config means no sensor should be used. // See also {@link com.android.server.display.utils.SensorUtils} @@ -174,7 +165,7 @@ public class SensorData { * Loads temperature sensor data for no config case. (Type: SKIN, Name: null) */ public static SensorData loadTempSensorUnspecifiedConfig() { - return new SensorData(TEMPERATURE_TYPE_SKIN, null); + return new SensorData(TEMPERATURE_TYPE_SKIN, /* name= */ null); } /** @@ -185,7 +176,7 @@ public class SensorData { DisplayConfiguration config) { SensorDetails sensorDetails = config.getTempSensor(); if (!flags.isSensorBasedBrightnessThrottlingEnabled() || sensorDetails == null) { - return new SensorData(TEMPERATURE_TYPE_SKIN, null); + return loadTempSensorUnspecifiedConfig(); } String name = sensorDetails.getName(); String type = sensorDetails.getType(); @@ -202,7 +193,7 @@ public class SensorData { */ @NonNull public static SensorData loadSensorUnspecifiedConfig() { - return new SensorData(); + return UNSPECIFIED_SENSOR_DATA; } private static SensorData loadSensorData(@NonNull SensorDetails sensorDetails) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 1f46af8b741d..bb2efa166800 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2224,12 +2224,6 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") - private void notifyConfigurationChanged(long whenNanos) { - mWindowManagerCallbacks.notifyConfigurationChanged(); - } - - // Native callback. - @SuppressWarnings("unused") private void notifyInputDevicesChanged(InputDevice[] inputDevices) { synchronized (mInputDevicesLock) { if (!mInputDevicesChangedPending) { @@ -2240,6 +2234,9 @@ public class InputManagerService extends IInputManager.Stub mInputDevices = inputDevices; } + // Input device change can possibly change configuration, so notify window manager to update + // its configuration. + mWindowManagerCallbacks.notifyConfigurationChanged(); } // Native callback. diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 96ca570fe984..cbe202b5adc8 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -22,8 +22,6 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS; -import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE; -import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED; import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION; import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD; import static android.server.inputmethod.InputMethodManagerServiceProto.CONCURRENT_MULTI_USER_MODE_ENABLED; @@ -51,6 +49,7 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT; import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO; @@ -597,7 +596,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } break; } - case STYLUS_HANDWRITING_ENABLED: { + case Settings.Secure.STYLUS_HANDWRITING_ENABLED: { InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches(); InputMethodManager .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches(); @@ -702,24 +701,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. super(true); } - @GuardedBy("ImfLock.class") - private boolean isChangingPackagesOfCurrentUserLocked() { - final int userId = getChangingUserId(); - final boolean retval = userId == mCurrentUserId; - if (DEBUG) { - if (!retval) { - Slog.d(TAG, "--- ignore this call back from a background user: " + userId); - } - } - return retval; - } - @Override public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { synchronized (ImfLock.class) { - if (!isChangingPackagesOfCurrentUserLocked()) { - return false; - } final int userId = getChangingUserId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); String curInputMethodId = settings.getSelectedInputMethod(); @@ -1114,13 +1098,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); InputMethodSettingsRepository.put(userId, newSettings); - if (mCurrentUserId == userId) { - // We need to rebuild IMEs. - postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); - updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId); - } else if (mConcurrentMultiUserModeEnabled) { - initializeVisibleBackgroundUserLocked(userId); - } + // We need to rebuild IMEs. + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); + updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId); } } @@ -1426,16 +1406,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final String defaultImiId = SecureSettingsWrapper.getString( Settings.Secure.DEFAULT_INPUT_METHOD, null, currentUserId); final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId); - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - currentUserId, AdditionalSubtypeMapRepository.get(currentUserId), - DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(currentUserId, newSettings); + final var settings = InputMethodSettingsRepository.get(currentUserId); postInputMethodSettingUpdatedLocked( !imeSelectedOnBoot /* resetDefaultEnabledIme */, currentUserId); updateFromSettingsLocked(true, currentUserId); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), - newSettings.getEnabledInputMethodList()); + settings.getEnabledInputMethodList()); AdditionalSubtypeMapRepository.startWriterThread(); @@ -1455,9 +1432,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. (windowToken, imeVisible) -> { if (Flags.refactorInsetsController()) { if (imeVisible) { - showSoftInputInternal(windowToken); + showCurrentInputInternal(windowToken); } else { - hideSoftInputInternal(windowToken); + hideCurrentInputInternal(windowToken); } } }); @@ -1506,17 +1483,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } + if (!mUserManagerInternal.exists(userId)) { + return InputMethodInfoSafeList.empty(); + } synchronized (ImfLock.class) { - final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mCurrentUserId, null); - if (resolvedUserIds.length != 1) { - return InputMethodInfoSafeList.empty(); - } final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { return InputMethodInfoSafeList.create(getInputMethodListLocked( - resolvedUserIds[0], directBootAwareness, callingUid)); + userId, directBootAwareness, callingUid)); } finally { Binder.restoreCallingIdentity(ident); } @@ -1531,17 +1506,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } + if (!mUserManagerInternal.exists(userId)) { + return InputMethodInfoSafeList.empty(); + } synchronized (ImfLock.class) { - final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mCurrentUserId, null); - if (resolvedUserIds.length != 1) { - return InputMethodInfoSafeList.empty(); - } final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { return InputMethodInfoSafeList.create( - getEnabledInputMethodListLocked(resolvedUserIds[0], callingUid)); + getEnabledInputMethodListLocked(userId, callingUid)); } finally { Binder.restoreCallingIdentity(ident); } @@ -1557,17 +1530,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } + if (!mUserManagerInternal.exists(userId)) { + return Collections.emptyList(); + } synchronized (ImfLock.class) { - final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mCurrentUserId, null); - if (resolvedUserIds.length != 1) { - return Collections.emptyList(); - } final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { - return getInputMethodListLocked( - resolvedUserIds[0], directBootAwareness, callingUid); + return getInputMethodListLocked(userId, directBootAwareness, callingUid); } finally { Binder.restoreCallingIdentity(ident); } @@ -1582,16 +1552,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } + if (!mUserManagerInternal.exists(userId)) { + return Collections.emptyList(); + } synchronized (ImfLock.class) { - final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mCurrentUserId, null); - if (resolvedUserIds.length != 1) { - return Collections.emptyList(); - } final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { - return getEnabledInputMethodListLocked(resolvedUserIds[0], callingUid); + return getEnabledInputMethodListLocked(userId, callingUid); } finally { Binder.restoreCallingIdentity(ident); } @@ -1631,8 +1599,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // If user is a profile, use preference of it`s parent profile. final int profileParentUserId = mUserManagerInternal.getProfileParentId(userId); if (Settings.Secure.getIntForUser(context.getContentResolver(), - STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE, - profileParentUserId) == 0) { + Settings.Secure.STYLUS_HANDWRITING_ENABLED, + Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE, profileParentUserId) == 0) { return false; } return true; @@ -1933,7 +1901,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (Flags.refactorInsetsController()) { if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null && userData.mImeBindingState.mFocusedWindow != null) { - showSoftInputInternal(userData.mImeBindingState.mFocusedWindow); + showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow); } } else { if (isShowRequestedForCurrentWindow(userId)) { @@ -3165,8 +3133,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - boolean showSoftInputInternal(IBinder windowToken) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal"); + boolean showCurrentInputInternal(IBinder windowToken) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#showSoftInput", mDumper); synchronized (ImfLock.class) { @@ -3185,8 +3153,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - boolean hideSoftInputInternal(IBinder windowToken) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal"); + boolean hideCurrentInputInternal(IBinder windowToken) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#hideSoftInput", mDumper); synchronized (ImfLock.class) { @@ -4801,8 +4769,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom( windowToken, userId); mVisibilityApplier.applyImeVisibility(requestToken, statsToken, - setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME - : ImeVisibilityStateComputer.STATE_HIDE_IME, + setVisible ? STATE_SHOW_IME : STATE_HIDE_IME, SoftInputShowHideReason.NOT_SET /* ignore reason */, userId); } } finally { @@ -6288,7 +6255,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. boolean isCritical) { IInputMethodInvoker method; ClientState client; - ClientState focusedWindowClient; final Printer p = new PrintWriterPrinter(pw); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java index cbb1807a6c2e..045414bde82e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java @@ -17,17 +17,22 @@ package com.android.server.inputmethod; +import static android.Manifest.permission.HIDE_OVERLAY_WINDOWS; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; + import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.os.UserHandle; import android.text.TextUtils; import android.util.Printer; import android.util.Slog; @@ -80,6 +85,7 @@ final class InputMethodMenuControllerNew { * @param displayId the ID of the display where the menu was requested. * @param userId the ID of the user that requested the menu. */ + @RequiresPermission(allOf = {INTERACT_ACROSS_USERS, HIDE_OVERLAY_WINDOWS}) void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId, @UserIdInt int userId) { // Hide the menu in case it was already showing. @@ -120,7 +126,7 @@ final class InputMethodMenuControllerNew { .requireViewById(com.android.internal.R.id.button1); languageSettingsButton.setVisibility(View.VISIBLE); languageSettingsButton.setOnClickListener(v -> { - v.getContext().startActivity(languageSettingsIntent); + v.getContext().startActivityAsUser(languageSettingsIntent, UserHandle.of(userId)); hide(displayId, userId); }); } diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 3d0b079c69c8..741513cf3c0b 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -616,9 +616,10 @@ public class LocaleManagerService extends SystemService { LocaleConfig resLocaleConfig = null; try { resLocaleConfig = LocaleConfig.fromContextIgnoringOverride( - mContext.createPackageContext(appPackageName, 0)); + mContext.createPackageContextAsUser(appPackageName, /* flags= */ 0, + UserHandle.of(userId))); } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "Unknown package name " + appPackageName); + Slog.e(TAG, "Unknown package name " + appPackageName + " for user " + userId); return; } final File file = getXmlFileNameForUser(appPackageName, userId); diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index 6e991b4db2b1..2e167efc4d81 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -376,6 +376,11 @@ public class LocationManagerService extends ILocationManager.Stub implements mContext.getContentResolver(), Settings.Global.LOCATION_ENABLE_STATIONARY_THROTTLE, defaultStationaryThrottlingSetting) != 0; + if (Flags.disableStationaryThrottling() && !( + Flags.keepGnssStationaryThrottling() && enableStationaryThrottling + && GPS_PROVIDER.equals(manager.getName()))) { + enableStationaryThrottling = false; + } if (enableStationaryThrottling) { realProvider = new StationaryThrottlingLocationProvider(manager.getName(), mInjector, realProvider); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index ed451ff0c194..3f4a9bb4d864 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -297,7 +297,10 @@ public class ContextHubService extends IContextHubService.Stub { } public boolean isExpired() { - return mTimestamp + ContextHubTransactionManager.RELIABLE_MESSAGE_TIMEOUT.toNanos() + return mTimestamp + + ContextHubTransactionManager + .RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT + .toNanos() < SystemClock.elapsedRealtimeNanos(); } } @@ -333,8 +336,14 @@ public class ContextHubService extends IContextHubService.Stub { return new IContextHubClientCallback.Stub() { private void finishCallback() { try { - IContextHubClient client = mDefaultClientMap.get(contextHubId); - client.callbackFinished(); + if (mDefaultClientMap != null && mDefaultClientMap.containsKey(contextHubId)) { + IContextHubClient client = mDefaultClientMap.get(contextHubId); + client.callbackFinished(); + } else { + Log.e(TAG, "Default client not found for hub (ID = " + contextHubId + "): " + + mDefaultClientMap == null ? "map was null" + : "map did not contain the hub"); + } } catch (RemoteException e) { Log.e( TAG, diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java index e6d330f85dfc..cd69ebaba766 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java @@ -56,6 +56,9 @@ import java.util.concurrent.atomic.AtomicInteger; public static final Duration RELIABLE_MESSAGE_TIMEOUT = Duration.ofSeconds(1); + public static final Duration RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT = + RELIABLE_MESSAGE_TIMEOUT.multipliedBy(3); + private static final int MAX_PENDING_REQUESTS = 10000; private static final int RELIABLE_MESSAGE_MAX_NUM_RETRY = 3; diff --git a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java index 17f2fcc5b9d8..bb35b378866b 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java @@ -340,6 +340,11 @@ class LockSettingsShellCommand extends ShellCommand { getOutPrintWriter().println("Profile uses unified challenge"); return false; } + if (mOld.isEmpty()) { + getOutPrintWriter().println( + "User has a lock credential, but old credential was not provided"); + return false; + } try { final boolean result = mLockPatternUtils.checkCredential(getOldCredential(), diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 803b125cbabc..621c090d37b8 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -702,7 +702,7 @@ public final class MediaProjectionManagerService extends SystemService } } - private final class BinderService extends IMediaProjectionManager.Stub { + final class BinderService extends IMediaProjectionManager.Stub { BinderService(Context context) { super(PermissionEnforcer.fromContext(context)); @@ -891,6 +891,13 @@ public final class MediaProjectionManagerService extends SystemService @Override public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) { requestConsentForInvalidProjection_enforcePermission(); + + if (android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions() + && mKeyguardManager.isKeyguardLocked()) { + Slog.v(TAG, "Reusing token: Won't request consent while the keyguard is locked"); + return; + } + synchronized (mLock) { if (!isCurrentProjection(projection)) { Slog.v(TAG, "Reusing token: Won't request consent again for a token that " diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java index 5ea3e70f7957..74f0d9cf3e39 100644 --- a/services/core/java/com/android/server/net/NetworkManagementService.java +++ b/services/core/java/com/android/server/net/NetworkManagementService.java @@ -81,8 +81,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IBatteryStats; import com.android.internal.util.DumpUtils; import com.android.internal.util.HexDump; -import com.android.modules.utils.build.SdkLevel; -import com.android.net.module.util.NetdUtils; import com.android.net.module.util.PermissionUtils; import com.android.server.FgThread; import com.android.server.LocalServices; @@ -833,144 +831,6 @@ public class NetworkManagementService extends INetworkManagementService.Stub { } @Override - public boolean getIpForwardingEnabled() throws IllegalStateException{ - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException( - "NMS#getIpForwardingEnabled not supported in V+"); - } - try { - return mNetdService.ipfwdEnabled(); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void setIpForwardingEnabled(boolean enable) { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException( - "NMS#setIpForwardingEnabled not supported in V+"); - } try { - if (enable) { - mNetdService.ipfwdEnableForwarding("tethering"); - } else { - mNetdService.ipfwdDisableForwarding("tethering"); - } - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void startTethering(String[] dhcpRange) { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#startTethering not supported in V+"); - } - try { - NetdUtils.tetherStart(mNetdService, true /* usingLegacyDnsProxy */, dhcpRange); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void stopTethering() { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#stopTethering not supported in V+"); - } - try { - mNetdService.tetherStop(); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public boolean isTetheringStarted() { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#isTetheringStarted not supported in V+"); - } - try { - return mNetdService.tetherIsEnabled(); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void tetherInterface(String iface) { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#tetherInterface not supported in V+"); - } - try { - final LinkAddress addr = getInterfaceConfig(iface).getLinkAddress(); - final IpPrefix dest = new IpPrefix(addr.getAddress(), addr.getPrefixLength()); - NetdUtils.tetherInterface(mNetdService, iface, dest); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void untetherInterface(String iface) { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#untetherInterface not supported in V+"); - } - try { - NetdUtils.untetherInterface(mNetdService, iface); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public String[] listTetheredInterfaces() { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException( - "NMS#listTetheredInterfaces not supported in V+"); - } - try { - return mNetdService.tetherInterfaceList(); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void enableNat(String internalInterface, String externalInterface) { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#enableNat not supported in V+"); - } - try { - mNetdService.tetherAddForward(internalInterface, externalInterface); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override - public void disableNat(String internalInterface, String externalInterface) { - PermissionUtils.enforceNetworkStackPermission(mContext); - if (SdkLevel.isAtLeastV()) { - throw new UnsupportedOperationException("NMS#disableNat not supported in V+"); - } - try { - mNetdService.tetherRemoveForward(internalInterface, externalInterface); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - } - - @Override public void setInterfaceQuota(String iface, long quotaBytes) { PermissionUtils.enforceNetworkStackPermission(mContext); @@ -1126,30 +986,19 @@ public class NetworkManagementService extends INetworkManagementService.Stub { } Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "setDataSaverModeEnabled"); try { - if (SdkLevel.isAtLeastV()) { - // setDataSaverEnabled throws if it fails to set data saver. - mContext.getSystemService(ConnectivityManager.class) - .setDataSaverEnabled(enable); - mDataSaverMode = enable; - if (mUseMeteredFirewallChains) { - // Copy mDataSaverMode state to FIREWALL_CHAIN_METERED_ALLOW - // until ConnectivityService allows manipulation of the data saver mode via - // FIREWALL_CHAIN_METERED_ALLOW. - synchronized (mRulesLock) { - mFirewallChainStates.put(FIREWALL_CHAIN_METERED_ALLOW, enable); - } - } - return true; - } else { - final boolean changed = mNetdService.bandwidthEnableDataSaver(enable); - if (changed) { - mDataSaverMode = enable; - } else { - Log.e(TAG, "setDataSaverMode(" + enable + "): failed to set iptables"); + // setDataSaverEnabled throws if it fails to set data saver. + mContext.getSystemService(ConnectivityManager.class).setDataSaverEnabled(enable); + mDataSaverMode = enable; + if (mUseMeteredFirewallChains) { + // Copy mDataSaverMode state to FIREWALL_CHAIN_METERED_ALLOW + // until ConnectivityService allows manipulation of the data saver mode via + // FIREWALL_CHAIN_METERED_ALLOW. + synchronized (mRulesLock) { + mFirewallChainStates.put(FIREWALL_CHAIN_METERED_ALLOW, enable); } - return changed; } - } catch (RemoteException | IllegalStateException e) { + return true; + } catch (IllegalStateException e) { Log.e(TAG, "setDataSaverMode(" + enable + "): failed with exception", e); return false; } finally { diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index d9e22c5a270f..53b67969e91a 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -4118,7 +4118,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { fout.increaseIndent(); for (int i = 0; i < mSubscriptionPlans.size(); i++) { final int subId = mSubscriptionPlans.keyAt(i); - fout.println("Subscriber ID " + subId + ":"); + fout.println("Subscription ID " + subId + ":"); fout.increaseIndent(); final SubscriptionPlan[] plans = mSubscriptionPlans.valueAt(i); if (!ArrayUtils.isEmpty(plans)) { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index c078409f468c..b12a917eede9 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1159,11 +1159,18 @@ public class ZenModeHelper { rule.conditionId = azr.getConditionId(); modified = true; } - boolean shouldPreserveCondition = Flags.modesApi() && Flags.modesUi() - && !isNew && origin == UPDATE_ORIGIN_USER - && rule.enabled == azr.isEnabled() - && rule.conditionId != null && rule.condition != null - && rule.conditionId.equals(rule.condition.id); + // This can be removed when {@link Flags#modesUi} is fully ramped up + final boolean isWatch = + mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); + boolean shouldPreserveCondition = + Flags.modesApi() + && (Flags.modesUi() || isWatch) + && !isNew + && origin == UPDATE_ORIGIN_USER + && rule.enabled == azr.isEnabled() + && rule.conditionId != null + && rule.condition != null + && rule.conditionId.equals(rule.condition.id); if (!shouldPreserveCondition) { // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR. rule.condition = null; diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java index 58b14b14fdef..15e758cf6ffd 100644 --- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java +++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java @@ -37,7 +37,6 @@ import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; -import android.os.Vibrator; import android.util.Log; import com.android.internal.R; @@ -47,9 +46,9 @@ public class BackgroundUserSoundNotifier { private static final boolean DEBUG = false; private static final String LOG_TAG = BackgroundUserSoundNotifier.class.getSimpleName(); - public static final String BUSN_CHANNEL_ID = "bg_user_sound_channel"; - public static final String BUSN_CHANNEL_NAME = "BackgroundUserSound"; - private static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER"; + private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel"; + private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound"; + public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER"; private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID"; private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID"; private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER"; @@ -144,6 +143,7 @@ public class BackgroundUserSoundNotifier { -1) + " current user id " + intent.getIntExtra( EXTRA_CURRENT_USER_ID, -1)); } + mUserWithNotification = -1; mNotificationManager.cancelAsUser(LOG_TAG, notificationId, UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1))); if (ACTION_MUTE_SOUND.equals(intent.getAction())) { @@ -159,10 +159,6 @@ public class BackgroundUserSoundNotifier { } } } - Vibrator vibrator = mSystemUserContext.getSystemService(Vibrator.class); - if (vibrator != null && vibrator.isVibrating()) { - vibrator.cancel(); - } } else if (ACTION_SWITCH_USER.equals(intent.getAction())) { service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1)); } diff --git a/services/core/java/com/android/server/pm/SaferIntentUtils.java b/services/core/java/com/android/server/pm/SaferIntentUtils.java index 8175321ea293..9a7ba0f082ea 100644 --- a/services/core/java/com/android/server/pm/SaferIntentUtils.java +++ b/services/core/java/com/android/server/pm/SaferIntentUtils.java @@ -104,6 +104,7 @@ public class SaferIntentUtils { @Disabled private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188; + @Nullable private static ParsedMainComponent infoToComponent( ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver) { if (info instanceof ActivityInfo) { @@ -186,7 +187,7 @@ public class SaferIntentUtils { } boolean isChangeEnabled(long changeId) { - return platformCompat == null || platformCompat.isChangeEnabledByUidInternal( + return platformCompat == null || platformCompat.isChangeEnabledByUidInternalNoLogging( changeId, callingUid); } @@ -233,7 +234,8 @@ public class SaferIntentUtils { } final ParsedMainComponent comp = infoToComponent( resolveInfo.getComponentInfo(), resolver, args.isReceiver); - if (!comp.getIntents().isEmpty() && args.intent.getAction() == null) { + if (comp != null && !comp.getIntents().isEmpty() + && args.intent.getAction() == null) { match = false; } } else if (c instanceof IntentFilter) { diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java index fde23b726572..9b644880d737 100644 --- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java +++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java @@ -16,6 +16,7 @@ package com.android.server.policy; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.role.RoleManager; import android.content.ActivityNotFoundException; @@ -248,31 +249,7 @@ public class ModifierShortcutManager { + " className=" + className + " shortcutChar=" + shortcutChar); continue; } - ComponentName componentName = new ComponentName(packageName, className); - try { - mPackageManager.getActivityInfo(componentName, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE - | PackageManager.MATCH_UNINSTALLED_PACKAGES); - } catch (PackageManager.NameNotFoundException e) { - String[] packages = mPackageManager.canonicalToCurrentPackageNames( - new String[] { packageName }); - componentName = new ComponentName(packages[0], className); - try { - mPackageManager.getActivityInfo(componentName, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE - | PackageManager.MATCH_UNINSTALLED_PACKAGES); - } catch (PackageManager.NameNotFoundException e1) { - Log.w(TAG, "Unable to add bookmark: " + packageName - + "/" + className + " not found."); - continue; - } - } - - intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setComponent(componentName); + intent = resolveComponentNameIntent(packageName, className); } else if (categoryName != null) { if (roleName != null) { Log.w(TAG, "Cannot specify role bookmark when category is present for" @@ -310,6 +287,32 @@ public class ModifierShortcutManager { } } + @Nullable + private Intent resolveComponentNameIntent(String packageName, String className) { + int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_UNINSTALLED_PACKAGES; + ComponentName componentName = new ComponentName(packageName, className); + try { + mPackageManager.getActivityInfo(componentName, flags); + } catch (PackageManager.NameNotFoundException e) { + String[] packages = mPackageManager.canonicalToCurrentPackageNames( + new String[] { packageName }); + componentName = new ComponentName(packages[0], className); + try { + mPackageManager.getActivityInfo(componentName, flags); + } catch (PackageManager.NameNotFoundException e1) { + Log.w(TAG, "Unable to add bookmark: " + packageName + + "/" + className + " not found."); + return null; + } + } + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(componentName); + return intent; + } + void registerShortcutKey(long shortcutCode, IShortcutService shortcutService) throws RemoteException { IShortcutService service = mShortcutKeyServices.get(shortcutCode); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 8419a608dc41..df973135f5f0 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2402,7 +2402,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); } - mWindowManagerInternal.registerAppTransitionListener(new AppTransitionListener() { + final var transitionListener = new AppTransitionListener(DEFAULT_DISPLAY) { @Override public int onAppTransitionStartingLocked(long statusBarAnimationStartTime, long statusBarAnimationDuration) { @@ -2436,7 +2436,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { mLockAfterDreamingTransitionFinished = false; } } - }); + }; + mWindowManagerInternal.registerAppTransitionListener(transitionListener); mKeyguardDrawnTimeout = mContext.getResources().getInteger( com.android.internal.R.integer.config_keyguardDrawnTimeout); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java index ad146afe16e7..fb54c5de260a 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java @@ -20,6 +20,8 @@ import android.os.BatteryStats; import android.os.BatteryUsageStats; import android.os.BatteryUsageStatsQuery; +import com.android.server.power.optimization.Flags; + public class BatteryStatsDumpHelperImpl implements BatteryStats.BatteryStatsDumpHelper { private final BatteryUsageStatsProvider mBatteryUsageStatsProvider; @@ -33,6 +35,9 @@ public class BatteryStatsDumpHelperImpl implements BatteryStats.BatteryStatsDump .setMaxStatsAgeMs(0); if (detailed) { builder.includePowerModels().includeProcessStateData().includeVirtualUids(); + if (Flags.batteryUsageStatsByPowerAndScreenState()) { + builder.includePowerStateData().includeScreenStateData(); + } } return mBatteryUsageStatsProvider.getBatteryUsageStats((BatteryStatsImpl) batteryStats, builder.build()); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 1b7bf89d7b44..4052a64aabba 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -16356,6 +16356,7 @@ public class BatteryStatsImpl extends BatteryStats { mBluetoothPowerStatsCollector.collectAndDump(pw); mCameraPowerStatsCollector.collectAndDump(pw); mGnssPowerStatsCollector.collectAndDump(pw); + mCustomEnergyConsumerPowerStatsCollector.collectAndDump(pw); } private final Runnable mWriteAsyncRunnable = () -> { diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java index 8127b8217bb0..ac6896696de6 100644 --- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java +++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java @@ -201,7 +201,8 @@ public class BatteryUsageStatsProvider { batteryUsageStatsBuilder = new BatteryUsageStats.Builder( stats.getCustomEnergyConsumerNames(), includePowerModels, - includeProcessStateData, minConsumedPowerThreshold); + includeProcessStateData, query.isScreenStateDataNeeded(), + query.isPowerStateDataNeeded(), minConsumedPowerThreshold); // TODO(b/188068523): use a monotonic clock to ensure resilience of order and duration // of batteryUsageStats sessions to wall-clock adjustments @@ -348,6 +349,7 @@ public class BatteryUsageStatsProvider { final String[] customEnergyConsumerNames = stats.getCustomEnergyConsumerNames(); final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( customEnergyConsumerNames, includePowerModels, includeProcessStateData, + query.isScreenStateDataNeeded(), query.isPowerStateDataNeeded(), minConsumedPowerThreshold); if (mPowerStatsStore == null) { Log.e(TAG, "PowerStatsStore is unavailable"); @@ -408,7 +410,6 @@ public class BatteryUsageStatsProvider { + " does not include process state data"); continue; } - builder.add(snapshot); } } diff --git a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java index 11919019be4a..0273ba6a6d18 100644 --- a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java +++ b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java @@ -19,6 +19,7 @@ package com.android.server.power.stats; import android.hardware.power.stats.EnergyConsumerType; import android.os.BatteryConsumer; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -65,4 +66,12 @@ public class CustomEnergyConsumerPowerStatsCollector extends PowerStatsCollector } return success; } + + @Override + public void collectAndDump(PrintWriter pw) { + ensureInitialized(); + for (int i = 0; i < mCollectors.size(); i++) { + mCollectors.get(i).collectAndDump(pw); + } + } } diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java index cace94114aa0..ce11fa0edaf7 100644 --- a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java +++ b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java @@ -176,9 +176,12 @@ public class EnergyConsumerPowerStatsCollector extends PowerStatsCollector { for (EnergyConsumerAttribution attribution : perUid) { int uid = mUidResolver.mapUid(attribution.uid); - long lastEnergy = mLastConsumerEnergyPerUid.get(uid); - long deltaEnergy = attribution.energyUWs - lastEnergy; + long lastEnergy = mLastConsumerEnergyPerUid.get(uid, ENERGY_UNSPECIFIED); mLastConsumerEnergyPerUid.put(uid, attribution.energyUWs); + if (lastEnergy == ENERGY_UNSPECIFIED) { + continue; + } + long deltaEnergy = attribution.energyUWs - lastEnergy; if (deltaEnergy <= 0) { continue; } @@ -189,7 +192,8 @@ public class EnergyConsumerPowerStatsCollector extends PowerStatsCollector { } mLayout.setUidConsumedEnergy(uidStats, 0, - mLayout.getUidConsumedEnergy(uidStats, 0) + deltaEnergy); + mLayout.getUidConsumedEnergy(uidStats, 0) + + uJtoUc(deltaEnergy, averageVoltage)); } } } diff --git a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java index d9f6c1ff1444..f5b00054bea4 100644 --- a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java +++ b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java @@ -196,12 +196,11 @@ public abstract class PowerStatsCollector { } IndentingPrintWriter out = new IndentingPrintWriter(pw); - out.print(getClass().getSimpleName()); if (!isEnabled()) { + out.print(getClass().getSimpleName()); out.println(": disabled"); return; } - out.println(); ArrayList<PowerStats> collected = new ArrayList<>(); Consumer<PowerStats> consumer = collected::add; @@ -215,11 +214,9 @@ public abstract class PowerStatsCollector { removeConsumer(consumer); } - out.increaseIndent(); for (PowerStats stats : collected) { stats.dump(out); } - out.decreaseIndent(); } private void awaitCompletion() { diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java index 549a97ea49cd..0f1349287a0f 100644 --- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java +++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java @@ -129,17 +129,55 @@ public class PowerStatsExporter { if (descriptor == null) { return; } + boolean isCustomComponent = + descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; PowerStatsLayout layout = new PowerStatsLayout(); layout.fromExtras(descriptor.extras); long[] deviceStats = new long[descriptor.statsArrayLength]; + for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) { + if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) { + if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + } else if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + + for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) { + if (batteryUsageStatsBuilder.isPowerStateDataNeeded() && !isCustomComponent) { + if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) { + continue; + } + } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) { + continue; + } + + populateAggregatedBatteryConsumer(batteryUsageStatsBuilder, powerComponentStats, + layout, deviceStats, screenState, powerState); + } + } + if (layout.isUidPowerAttributionSupported()) { + populateBatteryConsumers(batteryUsageStatsBuilder, + powerComponentStats, layout); + } + } + + private static void populateAggregatedBatteryConsumer( + BatteryUsageStats.Builder batteryUsageStatsBuilder, + PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout, + long[] deviceStats, @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + int powerComponentId = powerComponentStats.powerComponentId; + boolean isCustomComponent = + powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + double[] totalPower = new double[1]; MultiStateStats.States.forEachTrackedStateCombination( powerComponentStats.getConfig().getDeviceStateConfig(), states -> { - if (states[AggregatedPowerStatsConfig.STATE_POWER] - != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) { + if (!areMatchingStates(states, screenState, powerState)) { return; } @@ -153,24 +191,23 @@ public class PowerStatsExporter { AggregateBatteryConsumer.Builder deviceScope = batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder( BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); - if (descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { - if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent( - descriptor.powerComponentId)) { - deviceScope.addConsumedPowerForCustomComponent(descriptor.powerComponentId, - totalPower[0]); + if (isCustomComponent) { + if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) { + deviceScope.addConsumedPowerForCustomComponent(powerComponentId, totalPower[0]); } } else { - deviceScope.addConsumedPower(descriptor.powerComponentId, - totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED); - } - - if (layout.isUidPowerAttributionSupported()) { - populateUidBatteryConsumers(batteryUsageStatsBuilder, - powerComponentStats, layout); + BatteryConsumer.Key key = deviceScope.getKey(powerComponentId, + BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState); + if (key != null) { + deviceScope.addConsumedPower(key, totalPower[0], + BatteryConsumer.POWER_MODEL_UNDEFINED); + } + deviceScope.addConsumedPower(powerComponentId, totalPower[0], + BatteryConsumer.POWER_MODEL_UNDEFINED); } } - private static void populateUidBatteryConsumers( + private static void populateBatteryConsumers( BatteryUsageStats.Builder batteryUsageStatsBuilder, PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout) { @@ -185,11 +222,44 @@ public class PowerStatsExporter { .getUidStateConfig()[AggregatedPowerStatsConfig.STATE_PROCESS_STATE].isTracked() && powerComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + ArrayList<Integer> uids = new ArrayList<>(); + powerComponentStats.collectUids(uids); + for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) { + if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) { + if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + } else if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + + for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) { + if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) { + if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) { + continue; + } + } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) { + continue; + } + + populateUidBatteryConsumers(batteryUsageStatsBuilder, powerComponentStats, layout, + uids, powerComponent, uidStats, breakDownByProcState, screenState, + powerState); + } + } + } + + private static void populateUidBatteryConsumers( + BatteryUsageStats.Builder batteryUsageStatsBuilder, + PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout, + List<Integer> uids, AggregatedPowerStatsConfig.PowerComponent powerComponent, + long[] uidStats, boolean breakDownByProcState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + int powerComponentId = powerComponentStats.powerComponentId; double[] powerByProcState = new double[breakDownByProcState ? BatteryConsumer.PROCESS_STATE_COUNT : 1]; double powerAllApps = 0; - ArrayList<Integer> uids = new ArrayList<>(); - powerComponentStats.collectUids(uids); for (int uid : uids) { UidBatteryConsumer.Builder builder = batteryUsageStatsBuilder.getOrCreateUidBatteryConsumerBuilder(uid); @@ -199,8 +269,7 @@ public class PowerStatsExporter { MultiStateStats.States.forEachTrackedStateCombination( powerComponent.getUidStateConfig(), states -> { - if (states[AggregatedPowerStatsConfig.STATE_POWER] - != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) { + if (!areMatchingStates(states, screenState, powerState)) { return; } @@ -224,8 +293,17 @@ public class PowerStatsExporter { powerAllProcStates += power; if (breakDownByProcState && procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { - builder.addConsumedPower(builder.getKey(powerComponentId, procState), power, - BatteryConsumer.POWER_MODEL_UNDEFINED); + if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) { + builder.addConsumedPower( + builder.getKey(powerComponentId, procState, screenState, + powerState), + power, BatteryConsumer.POWER_MODEL_UNDEFINED); + } else { + builder.addConsumedPower( + builder.getKey(powerComponentId, procState, screenState, + BatteryConsumer.POWER_STATE_UNSPECIFIED), + power, BatteryConsumer.POWER_MODEL_UNDEFINED); + } } } if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { @@ -243,8 +321,49 @@ public class PowerStatsExporter { if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { allAppsScope.addConsumedPowerForCustomComponent(powerComponentId, powerAllApps); } else { + BatteryConsumer.Key key = allAppsScope.getKey(powerComponentId, + BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState); + if (key != null) { + allAppsScope.addConsumedPower(key, powerAllApps, + BatteryConsumer.POWER_MODEL_UNDEFINED); + } allAppsScope.addConsumedPower(powerComponentId, powerAllApps, BatteryConsumer.POWER_MODEL_UNDEFINED); } } + + private static boolean areMatchingStates(int[] states, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + switch (screenState) { + case BatteryConsumer.SCREEN_STATE_ON: + if (states[AggregatedPowerStatsConfig.STATE_SCREEN] + != AggregatedPowerStatsConfig.SCREEN_STATE_ON) { + return false; + } + break; + case BatteryConsumer.SCREEN_STATE_OTHER: + if (states[AggregatedPowerStatsConfig.STATE_SCREEN] + != AggregatedPowerStatsConfig.SCREEN_STATE_OTHER) { + return false; + } + break; + } + + switch (powerState) { + case BatteryConsumer.POWER_STATE_BATTERY: + if (states[AggregatedPowerStatsConfig.STATE_POWER] + != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) { + return false; + } + break; + case BatteryConsumer.POWER_STATE_OTHER: + if (states[AggregatedPowerStatsConfig.STATE_POWER] + != AggregatedPowerStatsConfig.POWER_STATE_OTHER) { + return false; + } + break; + } + return true; + } } diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index d34498a3764b..cc0a283db6a0 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -54,3 +54,17 @@ flag { description: "Adds battery_usage_stats_slice atom" bug: "324602949" } + +flag { + name: "battery_usage_stats_by_power_and_screen_state" + namespace: "backstage_power" + description: "Batterystats dumpsys is enhanced by including power break-down by power s" + bug: "352835319" +} + +flag { + name: "disable_composite_battery_usage_stats_atoms" + namespace: "backstage_power" + description: "Disable deprecated BatteryUsageStatsAtom pulled atom" + bug: "324602949" +} diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index 6537228583ec..5fab13bdc402 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -67,6 +67,7 @@ abstract class Vibration { CANCELLED_BY_SCREEN_OFF(VibrationProto.CANCELLED_BY_SCREEN_OFF), CANCELLED_BY_SETTINGS_UPDATE(VibrationProto.CANCELLED_BY_SETTINGS_UPDATE), CANCELLED_BY_USER(VibrationProto.CANCELLED_BY_USER), + CANCELLED_BY_FOREGROUND_USER(VibrationProto.CANCELLED_BY_FOREGROUND_USER), CANCELLED_BY_UNKNOWN_REASON(VibrationProto.CANCELLED_BY_UNKNOWN_REASON), CANCELLED_SUPERSEDED(VibrationProto.CANCELLED_SUPERSEDED), CANCELLED_BY_APP_OPS(VibrationProto.CANCELLED_BY_APP_OPS), diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 4437a2ddf3a7..bff175fec1dd 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import static android.os.VibrationAttributes.USAGE_CLASS_ALARM; import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; import static android.os.VibrationEffect.VibrationParameter.targetFrequency; @@ -73,6 +74,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.util.DumpUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.server.SystemService; +import com.android.server.pm.BackgroundUserSoundNotifier; import libcore.util.NativeAllocationRegistry; @@ -173,7 +175,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") @Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider; - private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @VisibleForTesting + BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { @@ -190,6 +193,19 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /* immediate= */ false); } } + } else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers() + && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) { + synchronized (mLock) { + if (shouldCancelOnFgUserRequest(mNextVibration)) { + clearNextVibrationLocked(new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_FOREGROUND_USER)); + } + if (shouldCancelOnFgUserRequest(mCurrentVibration)) { + mCurrentVibration.notifyCancelled(new Vibration.EndInfo( + Vibration.Status.CANCELLED_BY_FOREGROUND_USER), + /* immediate= */ false); + } + } } } }; @@ -299,6 +315,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); + if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers()) { + filter.addAction(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND); + } context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED); injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService()); @@ -1423,6 +1442,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") + private boolean shouldCancelOnFgUserRequest(@Nullable VibrationStepConductor conductor) { + if (conductor == null) { + return false; + } + return conductor.getVibration().callerInfo.attrs.getUsageClass() == USAGE_CLASS_ALARM; + } + + @GuardedBy("mLock") private void onAllVibratorsLocked(Consumer<VibratorController> consumer) { for (int i = 0; i < mVibrators.size(); i++) { consumer.accept(mVibrators.valueAt(i)); diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index a821f54520df..c4d601d03652 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -67,19 +67,13 @@ public class SystemImpl implements SystemInterface { private static final String TAG_SIGNATURE = "signature"; private static final String TAG_FALLBACK = "isFallback"; private static final String PIN_GROUP = "webview"; - private final WebViewProviderInfo[] mWebViewProviderPackages; - // Initialization-on-demand holder idiom for getting the WebView provider packages once and - // for all in a thread-safe manner. - private static class LazyHolder { - private static final SystemImpl INSTANCE = new SystemImpl(); - } + private final Context mContext; + private final WebViewProviderInfo[] mWebViewProviderPackages; - public static SystemImpl getInstance() { - return LazyHolder.INSTANCE; - } + SystemImpl(Context context) { + mContext = context; - private SystemImpl() { int numFallbackPackages = 0; int numAvailableByDefaultPackages = 0; XmlResourceParser parser = null; @@ -184,14 +178,14 @@ public class SystemImpl implements SystemInterface { } @Override - public String getUserChosenWebViewProvider(Context context) { - return Settings.Global.getString(context.getContentResolver(), + public String getUserChosenWebViewProvider() { + return Settings.Global.getString(mContext.getContentResolver(), Settings.Global.WEBVIEW_PROVIDER); } @Override - public void updateUserSetting(Context context, String newProviderName) { - Settings.Global.putString(context.getContentResolver(), + public void updateUserSetting(String newProviderName) { + Settings.Global.putString(mContext.getContentResolver(), Settings.Global.WEBVIEW_PROVIDER, newProviderName == null ? "" : newProviderName); } @@ -207,8 +201,8 @@ public class SystemImpl implements SystemInterface { } @Override - public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { - UserManager userManager = (UserManager)context.getSystemService(Context.USER_SERVICE); + public void enablePackageForAllUsers(String packageName, boolean enable) { + UserManager userManager = mContext.getSystemService(UserManager.class); for(UserInfo userInfo : userManager.getUsers()) { enablePackageForUser(packageName, enable, userInfo.id); } @@ -228,16 +222,15 @@ public class SystemImpl implements SystemInterface { } @Override - public void installExistingPackageForAllUsers(Context context, String packageName) { - UserManager userManager = context.getSystemService(UserManager.class); + public void installExistingPackageForAllUsers(String packageName) { + UserManager userManager = mContext.getSystemService(UserManager.class); for (UserInfo userInfo : userManager.getUsers()) { installPackageForUser(packageName, userInfo.id); } } private void installPackageForUser(String packageName, int userId) { - final Context context = AppGlobals.getInitialApplication(); - final Context contextAsUser = context.createContextAsUser(UserHandle.of(userId), 0); + final Context contextAsUser = mContext.createContextAsUser(UserHandle.of(userId), 0); final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller(); installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null); } @@ -255,29 +248,28 @@ public class SystemImpl implements SystemInterface { } @Override - public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, - WebViewProviderInfo configInfo) { - return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS); + public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo) { + return UserPackage.getPackageInfosAllUsers(mContext, configInfo.packageName, PACKAGE_FLAGS); } @Override - public int getMultiProcessSetting(Context context) { + public int getMultiProcessSetting() { if (updateServiceV2()) { throw new IllegalStateException( "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); } return Settings.Global.getInt( - context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); + mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); } @Override - public void setMultiProcessSetting(Context context, int value) { + public void setMultiProcessSetting(int value) { if (updateServiceV2()) { throw new IllegalStateException( "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); } Settings.Global.putInt( - context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); + mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); } @Override diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java index ad32f623c80d..3b77d07412ce 100644 --- a/services/core/java/com/android/server/webkit/SystemInterface.java +++ b/services/core/java/com/android/server/webkit/SystemInterface.java @@ -16,7 +16,6 @@ package com.android.server.webkit; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -34,19 +33,19 @@ import java.util.List; * @hide */ public interface SystemInterface { - public WebViewProviderInfo[] getWebViewPackages(); - public int onWebViewProviderChanged(PackageInfo packageInfo); - public long getFactoryPackageVersion(String packageName) throws NameNotFoundException; + WebViewProviderInfo[] getWebViewPackages(); + int onWebViewProviderChanged(PackageInfo packageInfo); + long getFactoryPackageVersion(String packageName) throws NameNotFoundException; - public String getUserChosenWebViewProvider(Context context); - public void updateUserSetting(Context context, String newProviderName); - public void killPackageDependents(String packageName); + String getUserChosenWebViewProvider(); + void updateUserSetting(String newProviderName); + void killPackageDependents(String packageName); - public void enablePackageForAllUsers(Context context, String packageName, boolean enable); - public void installExistingPackageForAllUsers(Context context, String packageName); + void enablePackageForAllUsers(String packageName, boolean enable); + void installExistingPackageForAllUsers(String packageName); - public boolean systemIsDebuggable(); - public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) + boolean systemIsDebuggable(); + PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) throws NameNotFoundException; /** * Get the PackageInfos of all users for the package represented by {@param configInfo}. @@ -54,15 +53,14 @@ public interface SystemInterface { * certain user. The returned array can contain null PackageInfos if the given package * is uninstalled for some user. */ - public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, - WebViewProviderInfo configInfo); + List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo); - public int getMultiProcessSetting(Context context); - public void setMultiProcessSetting(Context context, int value); - public void notifyZygote(boolean enableMultiProcess); + int getMultiProcessSetting(); + void setMultiProcessSetting(int value); + void notifyZygote(boolean enableMultiProcess); /** Start the zygote if it's not already running. */ - public void ensureZygoteStarted(); - public boolean isMultiProcessDefaultEnabled(); + void ensureZygoteStarted(); + boolean isMultiProcessDefaultEnabled(); - public void pinWebviewIfRequired(ApplicationInfo appInfo); + void pinWebviewIfRequired(ApplicationInfo appInfo); } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index 043470f62850..7acb864cbb40 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -73,9 +73,9 @@ public class WebViewUpdateService extends SystemService { public WebViewUpdateService(Context context) { super(context); if (updateServiceV2()) { - mImpl = new WebViewUpdateServiceImpl2(context, SystemImpl.getInstance()); + mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context)); } else { - mImpl = new WebViewUpdateServiceImpl(context, SystemImpl.getInstance()); + mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context)); } } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index dcf20f97ef71..b9be4a2deef2 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -16,7 +16,6 @@ package com.android.server.webkit; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -92,7 +91,6 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE; private final SystemInterface mSystemInterface; - private final Context mContext; private long mMinimumVersionCode = -1; @@ -110,8 +108,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private final Object mLock = new Object(); - WebViewUpdateServiceImpl(Context context, SystemInterface systemInterface) { - mContext = context; + WebViewUpdateServiceImpl(SystemInterface systemInterface) { mSystemInterface = systemInterface; } @@ -173,7 +170,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { try { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userSetting = mSystemInterface.getUserChosenWebViewProvider(); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { // Don't persist the user-chosen setting across boots if the package being @@ -181,8 +178,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { // be surprised by the device switching to using a certain webview package, // that was uninstalled/disabled a long time ago, if it is installed/enabled // again. - mSystemInterface.updateUserSetting(mContext, - mCurrentWebViewPackage.packageName); + mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); } onWebViewProviderChanged(mCurrentWebViewPackage); } @@ -203,8 +199,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); if (fallbackProvider != null) { Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName); - mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, - true); + mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true); } else { Slog.e(TAG, "No valid provider and no fallback available."); } @@ -316,7 +311,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { oldPackage = mCurrentWebViewPackage; if (newProviderName != null) { - mSystemInterface.updateUserSetting(mContext, newProviderName); + mSystemInterface.updateUserSetting(newProviderName); } try { @@ -447,7 +442,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos(); - String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(); // If the user has chosen provider, use that (if it's installed and enabled for all // users). @@ -455,7 +450,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { if (providerAndPackage.provider.packageName.equals(userChosenProvider)) { // userPackages can contain null objects. List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + mSystemInterface.getPackageInfoForProviderAllUsers( providerAndPackage.provider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return providerAndPackage.packageInfo; @@ -470,7 +465,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { if (providerAndPackage.provider.availableByDefault) { // userPackages can contain null objects. List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + mSystemInterface.getPackageInfoForProviderAllUsers( providerAndPackage.provider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return providerAndPackage.packageInfo; @@ -658,7 +653,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public boolean isMultiProcessEnabled() { - int settingValue = mSystemInterface.getMultiProcessSetting(mContext); + int settingValue = mSystemInterface.getMultiProcessSetting(); if (mSystemInterface.isMultiProcessDefaultEnabled()) { // Multiprocess should be enabled unless the user has turned it off manually. return settingValue > MULTIPROCESS_SETTING_OFF_VALUE; @@ -671,7 +666,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public void enableMultiProcess(boolean enable) { PackageInfo current = getCurrentWebViewPackage(); - mSystemInterface.setMultiProcessSetting(mContext, + mSystemInterface.setMultiProcessSetting( enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE); mSystemInterface.notifyZygote(enable); if (current != null) { @@ -725,7 +720,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { pw.println(" WebView packages:"); for (WebViewProviderInfo provider : allProviders) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + mSystemInterface.getPackageInfoForProviderAllUsers(provider); PackageInfo systemUserPackageInfo = userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); if (systemUserPackageInfo == null) { @@ -741,7 +736,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { systemUserPackageInfo.applicationInfo.targetSdkVersion); if (validity == VALIDITY_OK) { boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider)); + mSystemInterface.getPackageInfoForProviderAllUsers(provider)); pw.println(String.format( " Valid package %s (%s) is %s installed/enabled for all users", systemUserPackageInfo.packageName, diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index 993597eedd2c..307c15b72c76 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -16,7 +16,6 @@ package com.android.server.webkit; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -86,7 +85,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private static final int VALIDITY_NO_LIBRARY_FLAG = 4; private final SystemInterface mSystemInterface; - private final Context mContext; private final WebViewProviderInfo mDefaultProvider; private long mMinimumVersionCode = -1; @@ -108,8 +106,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private final Object mLock = new Object(); - WebViewUpdateServiceImpl2(Context context, SystemInterface systemInterface) { - mContext = context; + WebViewUpdateServiceImpl2(SystemInterface systemInterface) { mSystemInterface = systemInterface; WebViewProviderInfo[] webviewProviders = getWebViewPackages(); @@ -194,8 +191,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } if (mCurrentWebViewPackage.packageName.equals(mDefaultProvider.packageName)) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, mDefaultProvider); + mSystemInterface.getPackageInfoForProviderAllUsers(mDefaultProvider); return !isInstalledAndEnabledForAllUsers(userPackages); } else { return false; @@ -216,10 +212,8 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { TAG, "No provider available for all users, trying to install and enable " + mDefaultProvider.packageName); - mSystemInterface.installExistingPackageForAllUsers( - mContext, mDefaultProvider.packageName); - mSystemInterface.enablePackageForAllUsers( - mContext, mDefaultProvider.packageName, true); + mSystemInterface.installExistingPackageForAllUsers(mDefaultProvider.packageName); + mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true); } @Override @@ -229,7 +223,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); repairNeeded = shouldTriggerRepairLocked(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userSetting = mSystemInterface.getUserChosenWebViewProvider(); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { // Don't persist the user-chosen setting across boots if the package being @@ -237,8 +231,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { // be surprised by the device switching to using a certain webview package, // that was uninstalled/disabled a long time ago, if it is installed/enabled // again. - mSystemInterface.updateUserSetting(mContext, - mCurrentWebViewPackage.packageName); + mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); } onWebViewProviderChanged(mCurrentWebViewPackage); } @@ -362,7 +355,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { oldPackage = mCurrentWebViewPackage; if (newProviderName != null) { - mSystemInterface.updateUserSetting(mContext, newProviderName); + mSystemInterface.updateUserSetting(newProviderName); } try { @@ -493,7 +486,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { Counter.logIncrement("webview.value_find_preferred_webview_package_counter"); // If the user has chosen provider, use that (if it's installed and enabled for all // users). - String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(); WebViewProviderInfo userChosenProvider = getWebViewProviderForPackage(userChosenPackageName); if (userChosenProvider != null) { @@ -502,8 +495,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mSystemInterface.getPackageInfoForProvider(userChosenProvider); if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, userChosenProvider); + mSystemInterface.getPackageInfoForProviderAllUsers(userChosenProvider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return packageInfo; } @@ -779,7 +771,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { pw.println(" WebView packages:"); for (WebViewProviderInfo provider : allProviders) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + mSystemInterface.getPackageInfoForProviderAllUsers(provider); PackageInfo systemUserPackageInfo = userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); if (systemUserPackageInfo == null) { @@ -798,8 +790,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { if (validity == VALIDITY_OK) { boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, provider)); + mSystemInterface.getPackageInfoForProviderAllUsers(provider)); pw.println( TextUtils.formatSimple( " Valid package %s (%s) is %s installed/enabled for all users", diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index fa6ac651a059..516fc656ccb4 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -53,7 +53,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; -import static android.app.WindowConfiguration.isFloating; import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -336,7 +335,6 @@ import android.service.contentcapture.ActivityEvent; import android.service.dreams.DreamActivity; import android.service.voice.IVoiceInteractionSession; import android.util.ArraySet; -import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.MergedConfiguration; @@ -2867,7 +2865,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mStartingData != null) { if (mStartingData.mAssociatedTask != null) { // The snapshot type may have called associateStartingDataWithTask(). - attachStartingSurfaceToAssociatedTask(); + // If this activity is rotated, don't attach to task to preserve the transform. + if (!hasFixedRotationTransform()) { + attachStartingSurfaceToAssociatedTask(); + } } else if (isEmbedded()) { associateStartingWindowWithTaskIfNeeded(); } @@ -2898,6 +2899,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || mStartingData.mAssociatedTask != null) { return; } + if (task.isVisible() && !task.inTransition()) { + // Don't associated with task if the task is visible especially when the activity is + // embedded. We just need to show splash screen on the activity in case the first frame + // is not ready. + return; + } associateStartingDataWithTask(); attachStartingSurfaceToAssociatedTask(); } @@ -8639,7 +8646,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } - applySizeOverrideIfNeeded(newParentConfiguration, parentWindowingMode, resolvedConfig); + applySizeOverrideIfNeeded( + mDisplayContent, + info.applicationInfo, + newParentConfiguration, + resolvedConfig, + mOptOutEdgeToEdge, + hasFixedRotationTransform(), + getCompatDisplayInsets() != null); mResolveConfigHint.resetTmpOverrides(); logAppCompatState(); @@ -8649,100 +8663,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return Rect.copyOrNull(mResolveConfigHint.mParentAppBoundsOverride); } - /** - * If necessary, override configuration fields related to app bounds. - * This will happen when the app is targeting SDK earlier than 35. - * The insets and configuration has decoupled since SDK level 35, to make the system - * compatible to existing apps, override the configuration with legacy metrics. In legacy - * metrics, fields such as appBounds will exclude some of the system bar areas. - * The override contains all potentially affected fields in Configuration, including - * screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation. - * All overrides to those fields should be in this method. - * - * TODO: Consider integrate this with computeConfigByResolveHint() - */ - private void applySizeOverrideIfNeeded(Configuration newParentConfiguration, - int parentWindowingMode, Configuration inOutConfig) { - if (mDisplayContent == null) { - return; - } - final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds(); - int rotation = newParentConfiguration.windowConfiguration.getRotation(); - if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) { - rotation = mDisplayContent.getRotation(); - } - if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig - || getCompatDisplayInsets() != null - || (isFloating(parentWindowingMode) - // Check the requested windowing mode of activity as well in case it is - // switching between PiP and fullscreen. - && (inOutConfig.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_UNDEFINED - || isFloating(inOutConfig.windowConfiguration.getWindowingMode()))) - || rotation == ROTATION_UNDEFINED)) { - // If the insets configuration decoupled logic is not enabled for the app, or the app - // already has a compat override, or the context doesn't contain enough info to - // calculate the override, skip the override. - return; - } - // Make sure the orientation related fields will be updated by the override insets, because - // fixed rotation has assigned the fields from display's configuration. - if (hasFixedRotationTransform()) { - inOutConfig.windowConfiguration.setAppBounds(null); - inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED; - inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED; - inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; - inOutConfig.orientation = ORIENTATION_UNDEFINED; - } - - // Override starts here. - final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); - final int dw = rotated ? mDisplayContent.mBaseDisplayHeight - : mDisplayContent.mBaseDisplayWidth; - final int dh = rotated ? mDisplayContent.mBaseDisplayWidth - : mDisplayContent.mBaseDisplayHeight; - final Rect nonDecorInsets = mDisplayContent.getDisplayPolicy() - .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets; - // This should be the only place override the configuration for ActivityRecord. Override - // the value if not calculated yet. - Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); - if (outAppBounds == null || outAppBounds.isEmpty()) { - inOutConfig.windowConfiguration.setAppBounds(parentBounds); - outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); - outAppBounds.inset(nonDecorInsets); - } - float density = inOutConfig.densityDpi; - if (density == Configuration.DENSITY_DPI_UNDEFINED) { - density = newParentConfiguration.densityDpi; - } - density *= DisplayMetrics.DENSITY_DEFAULT_SCALE; - if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) { - inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f); - } - if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { - inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f); - } - if (inOutConfig.smallestScreenWidthDp - == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED - && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) { - // For the case of PIP transition and multi-window environment, the - // smallestScreenWidthDp is handled already. Override only if the app is in - // fullscreen. - final DisplayInfo info = new DisplayInfo(mDisplayContent.getDisplayInfo()); - mDisplayContent.computeSizeRanges(info, rotated, dw, dh, - mDisplayContent.getDisplayMetrics().density, - inOutConfig, true /* overrideConfig */); - } - - // It's possible that screen size will be considered in different orientation with or - // without considering the system bar insets. Override orientation as well. - if (inOutConfig.orientation == ORIENTATION_UNDEFINED) { - inOutConfig.orientation = - (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp) - ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; - } - } - private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig, @NonNull Configuration parentConfig) { task.computeConfigResourceOverrides(resolvedConfig, parentConfig, mResolveConfigHint); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 3b0b727597a5..26a6b00254d3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -584,7 +584,7 @@ public abstract class ActivityTaskManagerInternal { public abstract void clearLockedTasks(String reason); public abstract void updateUserConfiguration(); - public abstract boolean canShowErrorDialogs(); + public abstract boolean canShowErrorDialogs(int userId); public abstract void setProfileApp(String profileApp); public abstract void setProfileProc(WindowProcessController wpc); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ff46b33571f3..a84598dd73dc 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -190,6 +190,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -4899,14 +4900,21 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { * dialog / global actions also might want different behaviors. */ private void updateShouldShowDialogsLocked(Configuration config) { + mShowDialogs = shouldShowDialogs(config, /* checkUiMode= */ true); + } + + private boolean shouldShowDialogs(Configuration config, boolean checkUiMode) { final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS && config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH && config.navigation == Configuration.NAVIGATION_NONAV); final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(), HIDE_ERROR_DIALOGS, 0) != 0; - mShowDialogs = inputMethodExists - && ActivityTaskManager.currentUiModeSupportsErrorDialogs(config) - && !hideDialogsSet; + boolean showDialogs = inputMethodExists && !hideDialogsSet; + if (checkUiMode) { + showDialogs = showDialogs + && ActivityTaskManager.currentUiModeSupportsErrorDialogs(config); + } + return showDialogs; } private void updateFontScaleIfNeeded(@UserIdInt int userId) { @@ -7148,15 +7156,67 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public boolean canShowErrorDialogs() { + public boolean canShowErrorDialogs(int userId) { synchronized (mGlobalLock) { - return mShowDialogs && !mSleeping && !mShuttingDown + final boolean showDialogs = mShowDialogs + || shouldShowDialogsForVisibleBackgroundUserLocked(userId); + final UserInfo userInfo = getUserManager().getUserInfo(userId); + if (userInfo == null) { + // Unable to retrieve user information. Returning false, assuming there is + // no valid user with the given id. + return false; + } + return showDialogs && !mSleeping && !mShuttingDown && !mKeyguardController.isKeyguardOrAodShowing(DEFAULT_DISPLAY) - && !hasUserRestriction(UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS, - mAmInternal.getCurrentUserId()) + && !hasUserRestriction(UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS, userId) && !(UserManager.isDeviceInDemoMode(mContext) - && mAmInternal.getCurrentUser().isDemo()); + && userInfo.isDemo()); + } + } + + /** + * Checks if the given user is a visible background user, which is a full, background user + * assigned to secondary displays on the devices that have + * {@link UserManager#isVisibleBackgroundUsersEnabled() + * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on + * automotive builds, using the display associated with their seats). + * + * @see UserManager#isUserVisible() + */ + private boolean isVisibleBackgroundUser(int userId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return false; + } + boolean isForeground = getCurrentUserId() == userId; + boolean isProfile = getUserManager().isProfile(userId); + boolean isVisible = mWindowManager.mUmInternal.isUserVisible(userId); + return isVisible && !isForeground && !isProfile; + } + + /** + * In a car environment, {@link ActivityTaskManagerService#mShowDialogs} is always set to + * {@code false} from {@link ActivityTaskManagerService#updateShouldShowDialogsLocked} + * because its UI mode is {@link Configuration#UI_MODE_TYPE_CAR}. Thus, error dialogs are + * not displayed when an ANR or a crash occurs. However, in the automotive multi-user + * multi-display environment, this can confuse the passenger users and leave them + * uninformed when an app is terminated by the ANR or crash without any notification. + * To address this, error dialogs are allowed for the passenger users who have UI access + * on assigned displays (a.k.a. visible background users) on devices that have + * config_multiuserVisibleBackgroundUsers enabled even though the UI mode is + * {@link Configuration#UI_MODE_TYPE_CAR}. + * + * @see ActivityTaskManagerService#updateShouldShowDialogsLocked + */ + private boolean shouldShowDialogsForVisibleBackgroundUserLocked(int userId) { + if (!isVisibleBackgroundUser(userId)) { + return false; + } + final int displayId = mWindowManager.mUmInternal.getMainDisplayAssignedToUser(userId); + final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId); + if (dc == null) { + return false; } + return shouldShowDialogs(dc.getConfiguration(), /* checkUiMode= */ false); } @Override diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java index d9f11b1635cc..05d4c821c161 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java @@ -226,6 +226,14 @@ class AppCompatAspectRatioOverrides { : getDefaultMinAspectRatio(); } + float getDefaultMinAspectRatioForUnresizableAppsFromConfig() { + return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps(); + } + + boolean isSplitScreenAspectRatioForUnresizableAppsEnabled() { + return mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled(); + } + private float getDisplaySizeMinAspectRatio() { final DisplayArea displayArea = mActivityRecord.getDisplayArea(); if (displayArea == null) { @@ -278,7 +286,7 @@ class AppCompatAspectRatioOverrides { return getSplitScreenAspectRatio(); } - private float getDefaultMinAspectRatio() { + float getDefaultMinAspectRatio() { if (mActivityRecord.getDisplayArea() == null || !mAppCompatConfiguration .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) { diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 8421765060ce..0f8d68b713a7 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -166,15 +166,14 @@ class BackNavigationController { return null; } - // Move focus to the top embedded window if possible - if (mWindowManagerService.moveFocusToAdjacentEmbeddedWindow(window)) { - window = wmService.getFocusedWindowLocked(); - if (window == null) { - Slog.e(TAG, "New focused window is null, returning null."); - return null; - } + // Updating the window to the most recently used one among the embedded windows + // that are displayed adjacently, unless the IME is visible. + // When the IME is visible, the IME is displayed on top of embedded activities. + // In that case, the back event should still be delivered to focused activity in + // order to dismiss the IME. + if (!window.getDisplayContent().getImeContainer().isVisible()) { + window = mWindowManagerService.getMostRecentUsedEmbeddedWindowForBack(window); } - if (!window.isDrawn()) { ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focused window didn't have a valid surface drawn."); diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index efd52026cfec..3ebaf03c4a31 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -22,14 +22,23 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; +import static android.app.WindowConfiguration.isFloating; import static android.app.WindowConfiguration.windowingModeToString; import static android.app.WindowConfigurationProto.WINDOWING_MODE; import static android.content.ConfigurationProto.WINDOW_CONFIGURATION; +import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED; +import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import static com.android.server.wm.ConfigurationContainerProto.FULL_CONFIGURATION; import static com.android.server.wm.ConfigurationContainerProto.MERGED_OVERRIDE_CONFIGURATION; @@ -38,11 +47,14 @@ import static com.android.server.wm.ConfigurationContainerProto.OVERRIDE_CONFIGU import android.annotation.CallSuper; import android.annotation.NonNull; import android.app.WindowConfiguration; +import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.os.LocaleList; +import android.util.DisplayMetrics; import android.util.proto.ProtoOutputStream; +import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; @@ -173,6 +185,110 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { mResolvedOverrideConfiguration.setTo(mRequestedOverrideConfiguration); } + /** + * If necessary, override configuration fields related to app bounds. + * This will happen when the app is targeting SDK earlier than 35. + * The insets and configuration has decoupled since SDK level 35, to make the system + * compatible to existing apps, override the configuration with legacy metrics. In legacy + * metrics, fields such as appBounds will exclude some of the system bar areas. + * The override contains all potentially affected fields in Configuration, including + * screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation. + * All overrides to those fields should be in this method. + * + * TODO: Consider integrate this with computeConfigByResolveHint() + */ + static void applySizeOverrideIfNeeded(DisplayContent displayContent, ApplicationInfo appInfo, + Configuration newParentConfiguration, Configuration inOutConfig, + boolean optsOutEdgeToEdge, boolean hasFixedRotationTransform, + boolean hasCompatDisplayInsets) { + if (displayContent == null) { + return; + } + final boolean useOverrideInsetsForConfig = + displayContent.mWmService.mFlags.mInsetsDecoupledConfiguration + ? !appInfo.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + && !appInfo.isChangeEnabled( + OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION) + : appInfo.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION); + final int parentWindowingMode = + newParentConfiguration.windowConfiguration.getWindowingMode(); + final boolean isFloating = isFloating(parentWindowingMode) + // Check the requested windowing mode of activity as well in case it is + // switching between PiP and fullscreen. + && (inOutConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_UNDEFINED + || isFloating(inOutConfig.windowConfiguration.getWindowingMode())); + int rotation = newParentConfiguration.windowConfiguration.getRotation(); + if (rotation == ROTATION_UNDEFINED && !hasFixedRotationTransform) { + rotation = displayContent.getRotation(); + } + if (!optsOutEdgeToEdge && (!useOverrideInsetsForConfig + || hasCompatDisplayInsets + || isFloating + || rotation == ROTATION_UNDEFINED)) { + // If the insets configuration decoupled logic is not enabled for the app, or the app + // already has a compat override, or the context doesn't contain enough info to + // calculate the override, skip the override. + return; + } + // Make sure the orientation related fields will be updated by the override insets, because + // fixed rotation has assigned the fields from display's configuration. + if (hasFixedRotationTransform) { + inOutConfig.windowConfiguration.setAppBounds(null); + inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED; + inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED; + inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; + inOutConfig.orientation = ORIENTATION_UNDEFINED; + } + + // Override starts here. + final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); + final int dw = rotated + ? displayContent.mBaseDisplayHeight + : displayContent.mBaseDisplayWidth; + final int dh = rotated + ? displayContent.mBaseDisplayWidth + : displayContent.mBaseDisplayHeight; + // This should be the only place override the configuration for ActivityRecord. Override + // the value if not calculated yet. + Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); + if (outAppBounds == null || outAppBounds.isEmpty()) { + inOutConfig.windowConfiguration.setAppBounds( + newParentConfiguration.windowConfiguration.getBounds()); + outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); + outAppBounds.inset(displayContent.getDisplayPolicy() + .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets); + } + float density = inOutConfig.densityDpi; + if (density == Configuration.DENSITY_DPI_UNDEFINED) { + density = newParentConfiguration.densityDpi; + } + density *= DisplayMetrics.DENSITY_DEFAULT_SCALE; + if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) { + inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f); + } + if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { + inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f); + } + if (inOutConfig.smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED + && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) { + // For the case of PIP transition and multi-window environment, the + // smallestScreenWidthDp is handled already. Override only if the app is in + // fullscreen. + final DisplayInfo info = new DisplayInfo(displayContent.getDisplayInfo()); + displayContent.computeSizeRanges(info, rotated, dw, dh, + displayContent.getDisplayMetrics().density, + inOutConfig, true /* overrideConfig */); + } + + // It's possible that screen size will be considered in different orientation with or + // without considering the system bar insets. Override orientation as well. + if (inOutConfig.orientation == ORIENTATION_UNDEFINED) { + inOutConfig.orientation = (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp) + ? ORIENTATION_PORTRAIT + : ORIENTATION_LANDSCAPE; + } + } + /** Returns {@code true} if requested override override configuration is not empty. */ boolean hasRequestedOverrideConfiguration() { return mHasOverrideConfiguration; diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java index f9f5058415c6..3ecdff6b18a0 100644 --- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java +++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java @@ -16,18 +16,33 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.isFixedOrientationLandscape; +import static android.content.pm.ActivityInfo.isFixedOrientationPortrait; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; +import static com.android.server.wm.AppCompatUtils.computeAspectRatio; import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity; import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; +import android.app.AppCompatTaskInfo; +import android.app.TaskInfo; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.SystemProperties; import android.util.Size; import android.view.Gravity; +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; + import java.util.function.Consumer; /** @@ -38,6 +53,8 @@ public final class DesktopModeBoundsCalculator { public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f; + public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties + .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25); /** * Updates launch bounds for an activity with respect to its activity options, window layout, @@ -48,12 +65,8 @@ public final class DesktopModeBoundsCalculator { @NonNull Rect outBounds, @NonNull Consumer<String> logger) { // Use stable frame instead of raw frame to avoid launching freeform windows on top of // stable insets, which usually are system widgets such as sysbar & navbar. - final TaskDisplayArea displayArea = task.getDisplayArea(); - final Rect screenBounds = displayArea.getBounds(); final Rect stableBounds = new Rect(); - displayArea.getStableRect(stableBounds); - final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + task.getDisplayArea().getStableRect(stableBounds); if (options != null && options.getLaunchBounds() != null) { outBounds.set(options.getLaunchBounds()); @@ -63,37 +76,282 @@ public final class DesktopModeBoundsCalculator { final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; if (layout.hasSpecifiedSize()) { calculateLayoutBounds(stableBounds, layout, outBounds, - new Size(desiredWidth, desiredHeight)); + calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE)); applyLayoutGravity(verticalGravity, horizontalGravity, outBounds, stableBounds); logger.accept("layout specifies sizes, inheriting size and applying gravity"); } else if (verticalGravity > 0 || horizontalGravity > 0) { - calculateAndCentreInitialBounds(outBounds, screenBounds); + outBounds.set(calculateInitialBounds(task, activity, stableBounds)); applyLayoutGravity(verticalGravity, horizontalGravity, outBounds, stableBounds); logger.accept("layout specifies gravity, applying desired bounds and gravity"); } } else { - calculateAndCentreInitialBounds(outBounds, screenBounds); + outBounds.set(calculateInitialBounds(task, activity, stableBounds)); logger.accept("layout not specified, applying desired bounds"); } } /** - * Calculates the initial height and width of a task in desktop mode and centers it within the - * window bounds. + * Calculates the initial bounds required for an application to fill a scale of the display + * bounds without any letterboxing. This is done by taking into account the applications + * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is + * compatible with the applications previous configuration. + */ + private static @NonNull Rect calculateInitialBounds(@NonNull Task task, + @NonNull ActivityRecord activity, @NonNull Rect stableBounds + ) { + final TaskInfo taskInfo = task.getTaskInfo(); + // Display bounds not taking into account insets. + final TaskDisplayArea displayArea = task.getDisplayArea(); + final Rect screenBounds = displayArea.getBounds(); + final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + if (!Flags.enableWindowingDynamicInitialBounds()) { + return centerInScreen(idealSize, screenBounds); + } + // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete. + float appAspectRatio = calculateAspectRatio(task, activity); + final float tdaWidth = stableBounds.width(); + final float tdaHeight = stableBounds.height(); + final int activityOrientation = activity.getOverrideOrientation(); + final Size initialSize = switch (taskInfo.configuration.orientation) { + case ORIENTATION_LANDSCAPE -> { + // Device in landscape orientation. + if (appAspectRatio == 0) { + appAspectRatio = 1; + } + if (taskInfo.isResizeable) { + if (isFixedOrientationPortrait(activityOrientation)) { + // For portrait resizeable activities, respect apps fullscreen width but + // apply ideal size height. + yield new Size((int) ((tdaHeight / appAspectRatio) + 0.5f), + idealSize.getHeight()); + } + // For landscape resizeable activities, simply apply ideal size. + yield idealSize; + } + // If activity is unresizeable, regardless of orientation, calculate maximum size + // (within the ideal size) maintaining original aspect ratio. + yield maximizeSizeGivenAspectRatio( + activity.getOverrideOrientation(), idealSize, appAspectRatio); + } + case ORIENTATION_PORTRAIT -> { + // Device in portrait orientation. + final int customPortraitWidthForLandscapeApp = screenBounds.width() + - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); + if (taskInfo.isResizeable) { + if (isFixedOrientationLandscape(activityOrientation)) { + if (appAspectRatio == 0) { + appAspectRatio = tdaWidth / (tdaWidth - 1); + } + // For landscape resizeable activities, respect apps fullscreen height and + // apply custom app width. + yield new Size(customPortraitWidthForLandscapeApp, + (int) ((tdaWidth / appAspectRatio) + 0.5f)); + } + // For portrait resizeable activities, simply apply ideal size. + yield idealSize; + } + if (appAspectRatio == 0) { + appAspectRatio = 1; + } + if (isFixedOrientationLandscape(activityOrientation)) { + // For landscape unresizeable activities, apply custom app width to ideal size + // and calculate maximum size with this area while maintaining original aspect + // ratio. + yield maximizeSizeGivenAspectRatio(activityOrientation, + new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()), + appAspectRatio); + } + // For portrait unresizeable activities, calculate maximum size (within the ideal + // size) maintaining original aspect ratio. + yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio); + } + default -> idealSize; + }; + return centerInScreen(initialSize, screenBounds); + } + + /** + * Calculates the largest size that can fit in a given area while maintaining a specific aspect + * ratio. + */ + private static @NonNull Size maximizeSizeGivenAspectRatio( + @ActivityInfo.ScreenOrientation int orientation, + @NonNull Size targetArea, + float aspectRatio + ) { + final int targetHeight = targetArea.getHeight(); + final int targetWidth = targetArea.getWidth(); + final int finalHeight; + final int finalWidth; + if (isFixedOrientationPortrait(orientation)) { + // Portrait activity. + // Calculate required width given ideal height and aspect ratio. + int tempWidth = (int) (targetHeight / aspectRatio); + if (tempWidth <= targetWidth) { + // If the calculated width does not exceed the ideal width, overall size is within + // ideal size and can be applied. + finalHeight = targetHeight; + finalWidth = tempWidth; + } else { + // Applying target height cause overall size to exceed ideal size when maintain + // aspect ratio. Instead apply ideal width and calculate required height to respect + // aspect ratio. + finalWidth = targetWidth; + finalHeight = (int) (finalWidth * aspectRatio); + } + } else { + // Landscape activity. + // Calculate required width given ideal height and aspect ratio. + int tempWidth = (int) (targetHeight * aspectRatio); + if (tempWidth <= targetWidth) { + // If the calculated width does not exceed the ideal width, overall size is within + // ideal size and can be applied. + finalHeight = targetHeight; + finalWidth = tempWidth; + } else { + // Applying target height cause overall size to exceed ideal size when maintain + // aspect ratio. Instead apply ideal width and calculate required height to respect + // aspect ratio. + finalWidth = targetWidth; + finalHeight = (int) (finalWidth / aspectRatio); + } + } + return new Size(finalWidth, finalHeight); + } + + /** + * Calculates the aspect ratio of an activity from its fullscreen bounds. + */ + @VisibleForTesting + static float calculateAspectRatio(@NonNull Task task, @NonNull ActivityRecord activity) { + final TaskInfo taskInfo = task.getTaskInfo(); + final float fullscreenWidth = task.getDisplayArea().getBounds().width(); + final float fullscreenHeight = task.getDisplayArea().getBounds().height(); + final float maxAspectRatio = activity.getMaxAspectRatio(); + final float minAspectRatio = activity.getMinAspectRatio(); + float desiredAspectRatio = 0; + if (taskInfo.isRunning) { + final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; + if (appCompatTaskInfo.topActivityBoundsLetterboxed) { + desiredAspectRatio = (float) Math.max( + appCompatTaskInfo.topActivityLetterboxWidth, + appCompatTaskInfo.topActivityLetterboxHeight) + / Math.min(appCompatTaskInfo.topActivityLetterboxWidth, + appCompatTaskInfo.topActivityLetterboxHeight); + } else { + desiredAspectRatio = Math.max(fullscreenHeight, fullscreenWidth) + / Math.min(fullscreenHeight, fullscreenWidth); + } + } else { + final float letterboxAspectRatioOverride = + getFixedOrientationLetterboxAspectRatio(activity, task); + if (!task.mDisplayContent.getIgnoreOrientationRequest()) { + desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; + } else if (letterboxAspectRatioOverride + > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { + desiredAspectRatio = letterboxAspectRatioOverride; + } + } + // If the activity matches display orientation, the display aspect ratio should be used + if (activityMatchesDisplayOrientation( + taskInfo.configuration.orientation, + activity.getOverrideOrientation())) { + desiredAspectRatio = Math.max(fullscreenWidth, fullscreenHeight) + / Math.min(fullscreenWidth, fullscreenHeight); + } + if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) { + desiredAspectRatio = maxAspectRatio; + } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) { + desiredAspectRatio = minAspectRatio; + } + return desiredAspectRatio; + } + + private static boolean activityMatchesDisplayOrientation( + @Configuration.Orientation int deviceOrientation, + @ActivityInfo.ScreenOrientation int activityOrientation) { + if (deviceOrientation == ORIENTATION_PORTRAIT) { + return isFixedOrientationPortrait(activityOrientation); + } + return isFixedOrientationLandscape(activityOrientation); + } + + /** + * Calculates the desired initial bounds for applications in desktop windowing. This is done as + * a scale of the screen bounds. + */ + private static @NonNull Size calculateIdealSize(@NonNull Rect screenBounds, float scale) { + final int width = (int) (screenBounds.width() * scale); + final int height = (int) (screenBounds.height() * scale); + return new Size(width, height); + } + + /** + * Adjusts bounds to be positioned in the middle of the screen. */ - private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds, + private static @NonNull Rect centerInScreen(@NonNull Size desiredSize, @NonNull Rect screenBounds) { - // TODO(b/319819547): Account for app constraints so apps do not become letterboxed - // The desired dimensions that a fully resizable window should take when initially entering - // desktop mode. Calculated as a percentage of the available display area as defined by the - // DESKTOP_MODE_INITIAL_BOUNDS_SCALE. - final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - outBounds.right = desiredWidth; - outBounds.bottom = desiredHeight; - outBounds.offset(screenBounds.centerX() - outBounds.centerX(), - screenBounds.centerY() - outBounds.centerY()); + // TODO(b/325240051): Position apps with bottom heavy offset + final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2; + final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2; + final Rect resultBounds = new Rect(0, 0, + desiredSize.getWidth(), desiredSize.getHeight()); + resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset); + return resultBounds; + } + + private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity, + @NonNull Task task) { + return activity.shouldCreateCompatDisplayInsets() + ? getDefaultMinAspectRatioForUnresizableApps(activity, task) + : activity.mAppCompatController.getAppCompatAspectRatioOverrides() + .getDefaultMinAspectRatio(); + } + + private static float getDefaultMinAspectRatioForUnresizableApps( + @NonNull ActivityRecord activity, + @NonNull Task task) { + final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides = + activity.mAppCompatController.getAppCompatAspectRatioOverrides(); + if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) { + // Default letterbox aspect ratio for unresizable apps. + return getSplitScreenAspectRatio(activity, task); + } + + if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig() + > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { + return appCompatAspectRatioOverrides + .getDefaultMinAspectRatioForUnresizableAppsFromConfig(); + } + + return appCompatAspectRatioOverrides.getDefaultMinAspectRatio(); + } + + /** + * Calculates the aspect ratio of the available display area when an app enters split-screen on + * a given device, taking into account any dividers and insets. + */ + private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity, + @NonNull Task task) { + final int dividerWindowWidth = + activity.mWmService.mContext.getResources().getDimensionPixelSize( + R.dimen.docked_stack_divider_thickness); + final int dividerInsets = + activity.mWmService.mContext.getResources().getDimensionPixelSize( + R.dimen.docked_stack_divider_insets); + final int dividerSize = dividerWindowWidth - dividerInsets * 2; + final Rect bounds = new Rect(0, 0, + task.mDisplayContent.getDisplayInfo().appWidth, + task.mDisplayContent.getDisplayInfo().appHeight); + if (bounds.width() >= bounds.height()) { + bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0); + bounds.right = bounds.centerX(); + } else { + bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2); + bounds.bottom = bounds.centerY(); + } + return computeAspectRatio(bounds); } } diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index aacd3c65154f..548addbef39d 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -25,7 +25,6 @@ import android.annotation.Nullable; import android.app.ActivityOptions; import android.content.Context; import android.content.pm.ActivityInfo; -import android.os.SystemProperties; import android.util.Slog; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; @@ -38,19 +37,9 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM; private static final boolean DEBUG = false; - public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = - SystemProperties - .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f; - - /** - * Flag to indicate whether to restrict desktop mode to supported devices. - */ - private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( - "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); - private StringBuilder mLogBuilder; - private final Context mContext; + @NonNull private final Context mContext; DesktopModeLaunchParamsModifier(@NonNull Context context) { mContext = context; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 93711497f590..fa603682bc40 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3895,6 +3895,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Returns the focused window of the given Activity if the Activity is focused. + */ + WindowState findFocusedWindow(ActivityRecord activityRecord) { + final ActivityRecord tmpApp = mFocusedApp; + mTmpWindow = null; + try { + mFocusedApp = activityRecord; + // mFindFocusedWindow will populate mTmpWindow with the new focused window when found. + activityRecord.forAllWindows(mFindFocusedWindow, true /* traverseTopToBottom */); + } finally { + mFocusedApp = tmpApp; + } + return mTmpWindow; + } + + /** * Update the focused window and make some adjustments if the focus has changed. * * @param mode Indicates the situation we are in. Possible modes are: @@ -6911,6 +6927,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** Whether {@link #mAnimatingRecents} is going to be the top activity. */ private boolean mRecentsWillBeTop; + FixedRotationTransitionListener() { + super(DisplayContent.this.mDisplayId); + } + /** * If the recents activity has a fixed orientation which is different from the current top * activity, it will be rotated before being shown so we avoid a screen rotation animation diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 80362a44a33f..c3339cde6828 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -588,7 +588,7 @@ public class DisplayPolicy { gesturesPointerEventCallbacks); displayContent.registerPointerEventListener(mSystemGestures); } - mAppTransitionListener = new WindowManagerInternal.AppTransitionListener() { + mAppTransitionListener = new WindowManagerInternal.AppTransitionListener(displayId) { private Runnable mAppTransitionPending = () -> { StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index 63fe94c33061..e50a089a4d5e 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -44,6 +44,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.server.UiThread; +import com.android.window.flags.Flags; /** * Controls camera compatibility treatment that handles orientation mismatch between camera @@ -69,6 +70,9 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp @NonNull private final ActivityRefresher mActivityRefresher; + @Nullable + private Task mCameraTask; + @ScreenOrientation private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET; @@ -104,7 +108,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp * guaranteed to match, the rotation can cause letterboxing. * * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link - * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment. + * #isTreatmentEnabledForDisplay} for conditions enabling the treatment. */ @ScreenOrientation int getOrientation() { @@ -136,9 +140,9 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp // are aligned when they compute orientation of the preview. // This means that even for a landscape-only activity and a device with landscape natural // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that - // natural orientation = portrait window = portait camera is the main wrong assumption + // natural orientation = portrait window = portrait camera is the main wrong assumption // that apps make when they implement camera previews so landscape windows need be - // rotated in the orientation oposite to the natural one even if it's portrait. + // rotated in the orientation opposite to the natural one even if it's portrait. // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions // of the portrait and landscape orientation requests. final int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait) @@ -296,6 +300,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp @Override public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId) { + mCameraTask = cameraActivity.getTask(); // Checking whether an activity in fullscreen rather than the task as this camera // compat treatment doesn't cover activity embedding. if (cameraActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { @@ -305,7 +310,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp } // Checking that the whole app is in multi-window mode as we shouldn't show toast // for the activity embedding case. - if (cameraActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW + if (mCameraTask != null && mCameraTask.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW && isTreatmentEnabledForActivity( cameraActivity, /* mustBeFullscreen */ false)) { final PackageManager packageManager = mWmService.mContext.getPackageManager(); @@ -343,10 +348,15 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp @Override public boolean onCameraClosed(@NonNull String cameraId) { - // Top activity in the same task as the camera activity, or `null` if the task is - // closed. - final ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); + final ActivityRecord topActivity; + if (Flags.cameraCompatFullscreenPickSameTaskActivity()) { + topActivity = mCameraTask != null ? mCameraTask.getTopActivity( + /* includeFinishing= */ true, /* includeOverlays= */ false) : null; + } else { + topActivity = mDisplayContent.topRunningActivity(/* considerKeyguardState= */ true); + } + + mCameraTask = null; if (topActivity == null) { return true; } @@ -368,8 +378,6 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp mDisplayContent.mDisplayId); // Checking whether an activity in fullscreen rather than the task as this camera compat // treatment doesn't cover activity embedding. - // TODO(b/350495350): Consider checking whether this activity is the camera activity, or - // whether the top activity has the same task as the one which opened camera. if (topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { return true; } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 348384203fba..dcadb0f31085 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -397,9 +397,11 @@ class InsetsStateController { onRequestedVisibleTypesChanged(newControlTargets.valueAt(i)); } newControlTargets.clear(); - // Check for and try to run the scheduled show IME request (if it exists), as we - // now applied the surface transaction and notified the target of the new control. - getImeSourceProvider().checkAndStartShowImePostLayout(); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + // Check for and try to run the scheduled show IME request (if it exists), as we + // now applied the surface transaction and notified the target of the new control. + getImeSourceProvider().checkAndStartShowImePostLayout(); + } }); } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index c26684f60731..cc95518cce39 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -39,7 +39,6 @@ import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG; -import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.NonNull; @@ -537,27 +536,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { @Override public boolean startMovingTask(IWindow window, float startX, float startY) { - if (DEBUG_TASK_POSITIONING) Slog.d( - TAG_WM, "startMovingTask: {" + startX + "," + startY + "}"); - - final long ident = Binder.clearCallingIdentity(); - try { - return mService.mTaskPositioningController.startMovingTask(window, startX, startY); - } finally { - Binder.restoreCallingIdentity(ident); - } + return false; } @Override public void finishMovingTask(IWindow window) { - if (DEBUG_TASK_POSITIONING) Slog.d(TAG_WM, "finishMovingTask"); - - final long ident = Binder.clearCallingIdentity(); - try { - mService.mTaskPositioningController.finishTaskPositioning(window); - } finally { - Binder.restoreCallingIdentity(ident); - } } @Override diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java deleted file mode 100644 index 972dd2e382cc..000000000000 --- a/services/core/java/com/android/server/wm/TaskPositioner.java +++ /dev/null @@ -1,500 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.app.ActivityTaskManager.RESIZE_MODE_USER; -import static android.app.ActivityTaskManager.RESIZE_MODE_USER_FORCED; -import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; - -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; -import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; -import static com.android.server.wm.WindowManagerService.dipToPixel; -import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP; -import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -import android.annotation.NonNull; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.Binder; -import android.os.IBinder; -import android.os.InputConfig; -import android.os.RemoteException; -import android.os.Trace; -import android.util.DisplayMetrics; -import android.util.Slog; -import android.view.BatchedInputEventReceiver; -import android.view.InputApplicationHandle; -import android.view.InputChannel; -import android.view.InputDevice; -import android.view.InputEvent; -import android.view.InputEventReceiver; -import android.view.InputWindowHandle; -import android.view.MotionEvent; -import android.view.WindowManager; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.TaskResizingAlgorithm; -import com.android.internal.policy.TaskResizingAlgorithm.CtrlType; -import com.android.internal.protolog.ProtoLog; - -import java.util.concurrent.CompletableFuture; - -class TaskPositioner implements IBinder.DeathRecipient { - private static final boolean DEBUG_ORIENTATION_VIOLATIONS = false; - private static final String TAG_LOCAL = "TaskPositioner"; - private static final String TAG = TAG_WITH_CLASS_NAME ? TAG_LOCAL : TAG_WM; - - private static Factory sFactory; - - public static final float RESIZING_HINT_ALPHA = 0.5f; - - public static final int RESIZING_HINT_DURATION_MS = 0; - - private final WindowManagerService mService; - private InputEventReceiver mInputEventReceiver; - private DisplayContent mDisplayContent; - private Rect mTmpRect = new Rect(); - private int mMinVisibleWidth; - private int mMinVisibleHeight; - - @VisibleForTesting - Task mTask; - WindowState mWindow; - private boolean mResizing; - private boolean mPreserveOrientation; - private boolean mStartOrientationWasLandscape; - private final Rect mWindowOriginalBounds = new Rect(); - private final Rect mWindowDragBounds = new Rect(); - private final Point mMaxVisibleSize = new Point(); - private float mStartDragX; - private float mStartDragY; - @CtrlType - private int mCtrlType = CTRL_NONE; - @VisibleForTesting - boolean mDragEnded; - IBinder mClientCallback; - - InputChannel mClientChannel; - InputApplicationHandle mDragApplicationHandle; - InputWindowHandle mDragWindowHandle; - - /** Use {@link #create(WindowManagerService)} instead. */ - @VisibleForTesting - TaskPositioner(WindowManagerService service) { - mService = service; - } - - private boolean onInputEvent(InputEvent event) { - // All returns need to be in the try block to make sure the finishInputEvent is - // called correctly. - if (!(event instanceof MotionEvent) - || (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) { - return false; - } - final MotionEvent motionEvent = (MotionEvent) event; - if (mDragEnded) { - // The drag has ended but the clean-up message has not been processed by - // window manager. Drop events that occur after this until window manager - // has a chance to clean-up the input handle. - return true; - } - - final float newX = motionEvent.getRawX(); - final float newY = motionEvent.getRawY(); - - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_DOWN: { - if (DEBUG_TASK_POSITIONING) { - Slog.w(TAG, "ACTION_DOWN @ {" + newX + ", " + newY + "}"); - } - } - break; - - case MotionEvent.ACTION_MOVE: { - if (DEBUG_TASK_POSITIONING) { - Slog.w(TAG, "ACTION_MOVE @ {" + newX + ", " + newY + "}"); - } - synchronized (mService.mGlobalLock) { - mDragEnded = notifyMoveLocked(newX, newY); - mTask.getDimBounds(mTmpRect); - } - if (!mTmpRect.equals(mWindowDragBounds)) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, - "wm.TaskPositioner.resizeTask"); - mService.mAtmService.resizeTask( - mTask.mTaskId, mWindowDragBounds, RESIZE_MODE_USER); - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - } - } - break; - - case MotionEvent.ACTION_UP: { - if (DEBUG_TASK_POSITIONING) { - Slog.w(TAG, "ACTION_UP @ {" + newX + ", " + newY + "}"); - } - mDragEnded = true; - } - break; - - case MotionEvent.ACTION_CANCEL: { - if (DEBUG_TASK_POSITIONING) { - Slog.w(TAG, "ACTION_CANCEL @ {" + newX + ", " + newY + "}"); - } - mDragEnded = true; - } - break; - } - - if (mDragEnded) { - final boolean wasResizing = mResizing; - synchronized (mService.mGlobalLock) { - endDragLocked(); - mTask.getDimBounds(mTmpRect); - } - if (wasResizing && !mTmpRect.equals(mWindowDragBounds)) { - // We were using fullscreen surface during resizing. Request - // resizeTask() one last time to restore surface to window size. - mService.mAtmService.resizeTask( - mTask.mTaskId, mWindowDragBounds, RESIZE_MODE_USER_FORCED); - } - - // Post back to WM to handle clean-ups. We still need the input - // event handler for the last finishInputEvent()! - mService.mTaskPositioningController.finishTaskPositioning(); - } - return true; - } - - @VisibleForTesting - Rect getWindowDragBounds() { - return mWindowDragBounds; - } - - /** - * @param displayContent The Display that the window being dragged is on. - * @param win The window which will be dragged. - */ - CompletableFuture<Void> register(DisplayContent displayContent, @NonNull WindowState win) { - if (DEBUG_TASK_POSITIONING) { - Slog.d(TAG, "Registering task positioner"); - } - - if (mClientChannel != null) { - Slog.e(TAG, "Task positioner already registered"); - return completedFuture(null); - } - - mDisplayContent = displayContent; - mClientChannel = mService.mInputManager.createInputChannel(TAG); - - mInputEventReceiver = new BatchedInputEventReceiver.SimpleBatchedInputEventReceiver( - mClientChannel, mService.mAnimationHandler.getLooper(), - mService.mAnimator.getChoreographer(), this::onInputEvent); - - mDragApplicationHandle = new InputApplicationHandle(new Binder(), TAG, - DEFAULT_DISPATCHING_TIMEOUT_MILLIS); - - mDragWindowHandle = new InputWindowHandle(mDragApplicationHandle, - displayContent.getDisplayId()); - mDragWindowHandle.name = TAG; - mDragWindowHandle.token = mClientChannel.getToken(); - mDragWindowHandle.layoutParamsType = WindowManager.LayoutParams.TYPE_DRAG; - mDragWindowHandle.dispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; - mDragWindowHandle.ownerPid = WindowManagerService.MY_PID; - mDragWindowHandle.ownerUid = WindowManagerService.MY_UID; - mDragWindowHandle.scaleFactor = 1.0f; - // When dragging the window around, we do not want to steal focus for the window. - mDragWindowHandle.inputConfig = InputConfig.NOT_FOCUSABLE; - - // The drag window cannot receive new touches. - mDragWindowHandle.touchableRegion.setEmpty(); - - // Pause rotations before a drag. - ProtoLog.d(WM_DEBUG_ORIENTATION, "Pausing rotation during re-position"); - mDisplayContent.getDisplayRotation().pause(); - - // Notify InputMonitor to take mDragWindowHandle. - return mService.mTaskPositioningController.showInputSurface(win.getDisplayId()) - .thenRun(() -> { - // The global lock is held by the callers of register but released before the async - // results are waited on. We must acquire the lock in this callback to ensure thread - // safety. - synchronized (mService.mGlobalLock) { - final Rect displayBounds = mTmpRect; - displayContent.getBounds(displayBounds); - final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics(); - mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, displayMetrics); - mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, displayMetrics); - mMaxVisibleSize.set(displayBounds.width(), displayBounds.height()); - - mDragEnded = false; - - try { - mClientCallback = win.mClient.asBinder(); - mClientCallback.linkToDeath(this, 0 /* flags */); - } catch (RemoteException e) { - // The caller has died, so clean up TaskPositioningController. - mService.mTaskPositioningController.finishTaskPositioning(); - return; - } - mWindow = win; - mTask = win.getTask(); - } - }); - } - - void unregister() { - if (DEBUG_TASK_POSITIONING) { - Slog.d(TAG, "Unregistering task positioner"); - } - - if (mClientChannel == null) { - Slog.e(TAG, "Task positioner not registered"); - return; - } - - mService.mTaskPositioningController.hideInputSurface(mDisplayContent.getDisplayId()); - mService.mInputManager.removeInputChannel(mClientChannel.getToken()); - - mInputEventReceiver.dispose(); - mInputEventReceiver = null; - mClientChannel.dispose(); - mClientChannel = null; - - mDragWindowHandle = null; - mDragApplicationHandle = null; - mDragEnded = true; - - // Notify InputMonitor to remove mDragWindowHandle. - mDisplayContent.getInputMonitor().updateInputWindowsLw(true /*force*/); - - // Resume rotations after a drag. - ProtoLog.d(WM_DEBUG_ORIENTATION, "Resuming rotation after re-position"); - mDisplayContent.getDisplayRotation().resume(); - mDisplayContent = null; - if (mClientCallback != null) { - mClientCallback.unlinkToDeath(this, 0 /* flags */); - } - mWindow = null; - } - - /** - * Starts moving or resizing the task. This method should be only called from - * {@link TaskPositioningController#startPositioningLocked} or unit tests. - */ - void startDrag(boolean resize, boolean preserveOrientation, float startX, float startY) { - if (DEBUG_TASK_POSITIONING) { - Slog.d(TAG, "startDrag: win=" + mWindow + ", resize=" + resize - + ", preserveOrientation=" + preserveOrientation + ", {" + startX + ", " - + startY + "}"); - } - // Use the bounds of the task which accounts for - // multiple app windows. Don't use any bounds from win itself as it - // may not be the same size as the task. - final Rect startBounds = mTmpRect; - mTask.getBounds(startBounds); - - mCtrlType = CTRL_NONE; - mStartDragX = startX; - mStartDragY = startY; - mPreserveOrientation = preserveOrientation; - - if (resize) { - if (startX < startBounds.left) { - mCtrlType |= CTRL_LEFT; - } - if (startX > startBounds.right) { - mCtrlType |= CTRL_RIGHT; - } - if (startY < startBounds.top) { - mCtrlType |= CTRL_TOP; - } - if (startY > startBounds.bottom) { - mCtrlType |= CTRL_BOTTOM; - } - mResizing = mCtrlType != CTRL_NONE; - } - - // In case of !isDockedInEffect we are using the union of all task bounds. These might be - // made up out of multiple windows which are only partially overlapping. When that happens, - // the orientation from the window of interest to the entire stack might diverge. However - // for now we treat them as the same. - mStartOrientationWasLandscape = startBounds.width() >= startBounds.height(); - mWindowOriginalBounds.set(startBounds); - - // Notify the app that resizing has started, even though we haven't received any new - // bounds yet. This will guarantee that the app starts the backdrop renderer before - // configuration changes which could cause an activity restart. - if (mResizing) { - notifyMoveLocked(startX, startY); - - // The WindowPositionerEventReceiver callbacks are delivered on the same handler so this - // initial resize is always guaranteed to happen before subsequent drag resizes. - mService.mH.post(() -> { - mService.mAtmService.resizeTask( - mTask.mTaskId, startBounds, RESIZE_MODE_USER_FORCED); - }); - } - - // Make sure we always have valid drag bounds even if the drag ends before any move events - // have been handled. - mWindowDragBounds.set(startBounds); - } - - private void endDragLocked() { - mResizing = false; - mTask.setDragResizing(false); - } - - /** Returns true if the move operation should be ended. */ - @VisibleForTesting - boolean notifyMoveLocked(float x, float y) { - if (DEBUG_TASK_POSITIONING) { - Slog.d(TAG, "notifyMoveLocked: {" + x + "," + y + "}"); - } - - if (mCtrlType != CTRL_NONE) { - resizeDrag(x, y); - mTask.setDragResizing(true); - return false; - } - - // This is a moving or scrolling operation. - // Only allow to move in stable area so the target window won't be covered by system bar. - // Though {@link Task#resolveOverrideConfiguration} should also avoid the case. - mDisplayContent.getStableRect(mTmpRect); - // The task may be put in a limited display area. - mTmpRect.intersect(mTask.getRootTask().getParent().getBounds()); - - int nX = (int) x; - int nY = (int) y; - if (!mTmpRect.contains(nX, nY)) { - // For a moving operation we allow the pointer to go out of the stack bounds, but - // use the clamped pointer position for the drag bounds computation. - nX = Math.min(Math.max(nX, mTmpRect.left), mTmpRect.right); - nY = Math.min(Math.max(nY, mTmpRect.top), mTmpRect.bottom); - } - - updateWindowDragBounds(nX, nY, mTmpRect); - return false; - } - - /** - * The user is drag - resizing the window. - * - * @param x The x coordinate of the current drag coordinate. - * @param y the y coordinate of the current drag coordinate. - */ - @VisibleForTesting - void resizeDrag(float x, float y) { - updateDraggedBounds(TaskResizingAlgorithm.resizeDrag(x, y, mStartDragX, mStartDragY, - mWindowOriginalBounds, mCtrlType, mMinVisibleWidth, mMinVisibleHeight, - mMaxVisibleSize, mPreserveOrientation, mStartOrientationWasLandscape)); - } - - private void updateDraggedBounds(Rect newBounds) { - mWindowDragBounds.set(newBounds); - - checkBoundsForOrientationViolations(mWindowDragBounds); - } - - /** - * Validate bounds against orientation violations (if DEBUG_ORIENTATION_VIOLATIONS is set). - * - * @param bounds The bounds to be checked. - */ - private void checkBoundsForOrientationViolations(Rect bounds) { - // When using debug check that we are not violating the given constraints. - if (DEBUG_ORIENTATION_VIOLATIONS) { - if (mStartOrientationWasLandscape != (bounds.width() >= bounds.height())) { - Slog.e(TAG, "Orientation violation detected! should be " - + (mStartOrientationWasLandscape ? "landscape" : "portrait") - + " but is the other"); - } else { - Slog.v(TAG, "new bounds size: " + bounds.width() + " x " + bounds.height()); - } - if (mMinVisibleWidth > bounds.width() || mMinVisibleHeight > bounds.height()) { - Slog.v(TAG, "Minimum requirement violated: Width(min, is)=(" + mMinVisibleWidth - + ", " + bounds.width() + ") Height(min,is)=(" - + mMinVisibleHeight + ", " + bounds.height() + ")"); - } - if (mMaxVisibleSize.x < bounds.width() || mMaxVisibleSize.y < bounds.height()) { - Slog.v(TAG, "Maximum requirement violated: Width(min, is)=(" + mMaxVisibleSize.x - + ", " + bounds.width() + ") Height(min,is)=(" - + mMaxVisibleSize.y + ", " + bounds.height() + ")"); - } - } - } - - private void updateWindowDragBounds(int x, int y, Rect rootTaskBounds) { - final int offsetX = Math.round(x - mStartDragX); - final int offsetY = Math.round(y - mStartDragY); - mWindowDragBounds.set(mWindowOriginalBounds); - // Horizontally, at least mMinVisibleWidth pixels of the window should remain visible. - final int maxLeft = rootTaskBounds.right - mMinVisibleWidth; - final int minLeft = rootTaskBounds.left + mMinVisibleWidth - mWindowOriginalBounds.width(); - - // Vertically, the top mMinVisibleHeight of the window should remain visible. - // (This assumes that the window caption bar is at the top of the window). - final int minTop = rootTaskBounds.top; - final int maxTop = rootTaskBounds.bottom - mMinVisibleHeight; - - mWindowDragBounds.offsetTo( - Math.min(Math.max(mWindowOriginalBounds.left + offsetX, minLeft), maxLeft), - Math.min(Math.max(mWindowOriginalBounds.top + offsetY, minTop), maxTop)); - - if (DEBUG_TASK_POSITIONING) Slog.d(TAG, - "updateWindowDragBounds: " + mWindowDragBounds); - } - - public String toShortString() { - return TAG; - } - - static void setFactory(Factory factory) { - sFactory = factory; - } - - static TaskPositioner create(WindowManagerService service) { - if (sFactory == null) { - sFactory = new Factory() {}; - } - - return sFactory.create(service); - } - - @Override - public void binderDied() { - mService.mTaskPositioningController.finishTaskPositioning(); - } - - interface Factory { - default TaskPositioner create(WindowManagerService service) { - return new TaskPositioner(service); - } - } -} diff --git a/services/core/java/com/android/server/wm/TaskPositioningController.java b/services/core/java/com/android/server/wm/TaskPositioningController.java deleted file mode 100644 index 6f548ab01d74..000000000000 --- a/services/core/java/com/android/server/wm/TaskPositioningController.java +++ /dev/null @@ -1,250 +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.server.wm; - -import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; - -import static java.util.concurrent.CompletableFuture.completedFuture; - -import android.annotation.Nullable; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.Slog; -import android.view.Display; -import android.view.IWindow; -import android.view.InputWindowHandle; -import android.view.SurfaceControl; - -import java.util.concurrent.CompletableFuture; - -/** - * Controller for task positioning by drag. - */ -class TaskPositioningController { - private final WindowManagerService mService; - private SurfaceControl mInputSurface; - private DisplayContent mPositioningDisplay; - - private @Nullable TaskPositioner mTaskPositioner; - - private final Rect mTmpClipRect = new Rect(); - - boolean isPositioningLocked() { - return mTaskPositioner != null; - } - - final SurfaceControl.Transaction mTransaction; - - InputWindowHandle getDragWindowHandleLocked() { - return mTaskPositioner != null ? mTaskPositioner.mDragWindowHandle : null; - } - - TaskPositioningController(WindowManagerService service) { - mService = service; - mTransaction = service.mTransactionFactory.get(); - } - - void hideInputSurface(int displayId) { - if (mPositioningDisplay != null && mPositioningDisplay.getDisplayId() == displayId - && mInputSurface != null) { - mTransaction.hide(mInputSurface).apply(); - } - } - - /** - * @return a future that completes after window info is sent. - */ - CompletableFuture<Void> showInputSurface(int displayId) { - if (mPositioningDisplay == null || mPositioningDisplay.getDisplayId() != displayId) { - return completedFuture(null); - } - final DisplayContent dc = mService.mRoot.getDisplayContent(displayId); - if (mInputSurface == null) { - mInputSurface = mService.makeSurfaceBuilder(dc.getSession()) - .setContainerLayer() - .setName("Drag and Drop Input Consumer") - .setCallsite("TaskPositioningController.showInputSurface") - .setParent(dc.getOverlayLayer()) - .build(); - } - - final InputWindowHandle h = getDragWindowHandleLocked(); - if (h == null) { - Slog.w(TAG_WM, "Drag is in progress but there is no " - + "drag window handle."); - return completedFuture(null); - } - - final Display display = dc.getDisplay(); - final Point p = new Point(); - display.getRealSize(p); - mTmpClipRect.set(0, 0, p.x, p.y); - - CompletableFuture<Void> result = new CompletableFuture<>(); - mTransaction.show(mInputSurface) - .setInputWindowInfo(mInputSurface, h) - .setLayer(mInputSurface, Integer.MAX_VALUE) - .setPosition(mInputSurface, 0, 0) - .setCrop(mInputSurface, mTmpClipRect) - .addWindowInfosReportedListener(() -> result.complete(null)) - .apply(); - return result; - } - - boolean startMovingTask(IWindow window, float startX, float startY) { - WindowState win = null; - CompletableFuture<Boolean> startPositioningLockedFuture; - synchronized (mService.mGlobalLock) { - win = mService.windowForClientLocked(null, window, false); - startPositioningLockedFuture = - startPositioningLocked( - win, false /*resize*/, false /*preserveOrientation*/, startX, startY); - } - - try { - if (!startPositioningLockedFuture.get()) { - return false; - } - } catch (Exception exception) { - Slog.e(TAG_WM, "Exception thrown while waiting for startPositionLocked future", - exception); - return false; - } - - synchronized (mService.mGlobalLock) { - mService.mAtmService.setFocusedTask(win.getTask().mTaskId); - } - return true; - } - - void handleTapOutsideTask(DisplayContent displayContent, int x, int y) { - mService.mH.post(() -> { - Task task; - CompletableFuture<Boolean> startPositioningLockedFuture; - synchronized (mService.mGlobalLock) { - task = displayContent.findTaskForResizePoint(x, y); - if (task == null || !task.isResizeable()) { - // The task is not resizable, so don't do anything when the user drags the - // the resize handles. - return; - } - startPositioningLockedFuture = - startPositioningLocked(task.getTopVisibleAppMainWindow(), true /*resize*/, - task.preserveOrientationOnResize(), x, y); - } - - try { - if (!startPositioningLockedFuture.get()) { - return; - } - } catch (Exception exception) { - Slog.e(TAG_WM, "Exception thrown while waiting for startPositionLocked future", - exception); - return; - } - - synchronized (mService.mGlobalLock) { - mService.mAtmService.setFocusedTask(task.mTaskId); - } - }); - } - - private CompletableFuture<Boolean> startPositioningLocked(WindowState win, boolean resize, - boolean preserveOrientation, float startX, float startY) { - if (DEBUG_TASK_POSITIONING) - Slog.d(TAG_WM, "startPositioningLocked: " - + "win=" + win + ", resize=" + resize + ", preserveOrientation=" - + preserveOrientation + ", {" + startX + ", " + startY + "}"); - - if (win == null || win.mActivityRecord == null) { - Slog.w(TAG_WM, "startPositioningLocked: Bad window " + win); - return completedFuture(false); - } - if (win.mInputChannel == null) { - Slog.wtf(TAG_WM, "startPositioningLocked: " + win + " has no input channel, " - + " probably being removed"); - return completedFuture(false); - } - - final DisplayContent displayContent = win.getDisplayContent(); - if (displayContent == null) { - Slog.w(TAG_WM, "startPositioningLocked: Invalid display content " + win); - return completedFuture(false); - } - mPositioningDisplay = displayContent; - - mTaskPositioner = TaskPositioner.create(mService); - return mTaskPositioner.register(displayContent, win).thenApply(unused -> { - // The global lock is held by the callers of startPositioningLocked but released before - // the async results are waited on. We must acquire the lock in this callback to ensure - // thread safety. - synchronized (mService.mGlobalLock) { - // We need to grab the touch focus so that the touch events during the - // resizing/scrolling are not sent to the app. 'win' is the main window - // of the app, it may not have focus since there might be other windows - // on top (eg. a dialog window). - WindowState transferTouchFromWin = win; - if (displayContent.mCurrentFocus != null && displayContent.mCurrentFocus != win - && displayContent.mCurrentFocus.mActivityRecord == win.mActivityRecord) { - transferTouchFromWin = displayContent.mCurrentFocus; - } - if (!mService.mInputManager.transferTouchGesture( - transferTouchFromWin.mInputChannel.getToken(), - mTaskPositioner.mClientChannel.getToken())) { - Slog.e(TAG_WM, "startPositioningLocked: Unable to transfer touch focus"); - cleanUpTaskPositioner(); - return false; - } - - mTaskPositioner.startDrag(resize, preserveOrientation, startX, startY); - return true; - } - }); - } - - public void finishTaskPositioning(IWindow window) { - if (mTaskPositioner != null && mTaskPositioner.mClientCallback == window.asBinder()) { - finishTaskPositioning(); - } - } - - void finishTaskPositioning() { - // TaskPositioner attaches the InputEventReceiver to the animation thread. We need to - // dispose the receiver on the same thread to avoid race conditions. - mService.mAnimationHandler.post(() -> { - if (DEBUG_TASK_POSITIONING) Slog.d(TAG_WM, "finishPositioning"); - - synchronized (mService.mGlobalLock) { - cleanUpTaskPositioner(); - mPositioningDisplay = null; - } - }); - } - - private void cleanUpTaskPositioner() { - final TaskPositioner positioner = mTaskPositioner; - if (positioner == null) { - return; - } - - // We need to assign task positioner to null first to indicate that we're finishing task - // positioning. - mTaskPositioner = null; - positioner.unregister(); - } -} diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f6a68d58ea27..65bc9a226f55 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -745,6 +745,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (mController.isAnimating()) { dc.enableHighPerfTransition(true); } + mController.dispatchLegacyAppTransitionPending(dc.mDisplayId); } /** @@ -1618,7 +1619,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.mTransitionTracer.logAbortedTransition(this); // Syncengine abort will call through to onTransactionReady() mSyncEngine.abort(mSyncId); - mController.dispatchLegacyAppTransitionCancelled(); + mController.dispatchLegacyAppTransitionCancelled(mTargetDisplays); invokeTransitionEndedListeners(); } @@ -1766,7 +1767,19 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } for (int i = 0; i < mTargets.size(); ++i) { - final DisplayArea da = mTargets.get(i).mContainer.asDisplayArea(); + final WindowContainer<?> wc = mTargets.get(i).mContainer; + final WallpaperWindowToken wp = wc.asWallpaperToken(); + if (wp != null) { + // If on a rotation leash, the wallpaper token surface needs to be shown explicitly + // because shell only gets the leash and the wallpaper token surface is not allowed + // to be changed by non-transition logic until the transition is finished. + if (Flags.ensureWallpaperInTransitions() && wp.isVisibleRequested() + && wp.getFixedRotationLeash() != null) { + transaction.show(wp.mSurfaceControl); + } + continue; + } + final DisplayArea<?> da = wc.asDisplayArea(); if (da == null) continue; if (da.isVisibleRequested()) { mController.mValidateDisplayVis.remove(da); @@ -2168,14 +2181,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { && !wallpaperIsOwnTarget(wallpaper)) { wallpaper.setVisibleRequested(false); } - if (showWallpaper && Flags.ensureWallpaperInTransitions() - && wallpaper.isVisibleRequested() - && getLeashSurface(wallpaper, t) != wallpaper.getSurfaceControl()) { - // If on a rotation leash, we need to explicitly show the wallpaper surface - // because shell only gets the leash and we don't allow non-transition logic - // to touch the surfaces until the transition is over. - t.show(wallpaper.getSurfaceControl()); - } } } } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index f4ff404c2bff..1df251cf3225 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -42,6 +42,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; +import android.view.Display; import android.view.WindowManager; import android.window.ITransitionMetricsReporter; import android.window.ITransitionPlayer; @@ -326,7 +327,6 @@ class TransitionController { mCollectingTransition.startCollecting(timeoutMs); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Start collecting in Transition: %s", mCollectingTransition); - dispatchLegacyAppTransitionPending(); } void registerTransitionPlayer(@Nullable ITransitionPlayer player, @@ -1347,31 +1347,54 @@ class TransitionController { mLegacyListeners.remove(listener); } - void dispatchLegacyAppTransitionPending() { + private static boolean shouldDispatchLegacyListener( + WindowManagerInternal.AppTransitionListener listener, int displayId) { + // INVALID_DISPLAY means that it is a global listener. + return listener.mDisplayId == Display.INVALID_DISPLAY || listener.mDisplayId == displayId; + } + + void dispatchLegacyAppTransitionPending(int displayId) { for (int i = 0; i < mLegacyListeners.size(); ++i) { - mLegacyListeners.get(i).onAppTransitionPendingLocked(); + final WindowManagerInternal.AppTransitionListener listener = mLegacyListeners.get(i); + if (shouldDispatchLegacyListener(listener, displayId)) { + listener.onAppTransitionPendingLocked(); + } } } void dispatchLegacyAppTransitionStarting(TransitionInfo info, long statusBarTransitionDelay) { + final long now = SystemClock.uptimeMillis(); for (int i = 0; i < mLegacyListeners.size(); ++i) { - // TODO(shell-transitions): handle (un)occlude transition. - mLegacyListeners.get(i).onAppTransitionStartingLocked( - SystemClock.uptimeMillis() + statusBarTransitionDelay, - AnimationAdapter.STATUS_BAR_TRANSITION_DURATION); + final WindowManagerInternal.AppTransitionListener listener = mLegacyListeners.get(i); + for (int j = 0; j < info.getRootCount(); ++j) { + final int displayId = info.getRoot(j).getDisplayId(); + if (shouldDispatchLegacyListener(listener, displayId)) { + listener.onAppTransitionStartingLocked( + now + statusBarTransitionDelay, + AnimationAdapter.STATUS_BAR_TRANSITION_DURATION); + } + } } } void dispatchLegacyAppTransitionFinished(ActivityRecord ar) { for (int i = 0; i < mLegacyListeners.size(); ++i) { - mLegacyListeners.get(i).onAppTransitionFinishedLocked(ar.token); + final WindowManagerInternal.AppTransitionListener listener = mLegacyListeners.get(i); + if (shouldDispatchLegacyListener(listener, ar.getDisplayId())) { + listener.onAppTransitionFinishedLocked(ar.token); + } } } - void dispatchLegacyAppTransitionCancelled() { - for (int i = 0; i < mLegacyListeners.size(); ++i) { - mLegacyListeners.get(i).onAppTransitionCancelledLocked( - false /* keyguardGoingAwayCancelled */); + void dispatchLegacyAppTransitionCancelled(ArrayList<DisplayContent> targetDisplays) { + for (int i = 0; i < targetDisplays.size(); ++i) { + final int displayId = targetDisplays.get(i).mDisplayId; + for (int j = 0; j < mLegacyListeners.size(); ++j) { + final var listener = mLegacyListeners.get(j); + if (shouldDispatchLegacyListener(listener, displayId)) { + listener.onAppTransitionCancelledLocked(false /* keyguardGoingAwayCancelled */); + } + } } } diff --git a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java index 42b556f77ab6..61253602c066 100644 --- a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java +++ b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java @@ -47,7 +47,6 @@ public class WindowManagerDebugConfig { static final boolean DEBUG_LAYOUT_REPEATS = false; static final boolean DEBUG_WINDOW_TRACE = false; static final boolean DEBUG_TASK_MOVEMENT = false; - static final boolean DEBUG_TASK_POSITIONING = false; static final boolean DEBUG_ROOT_TASK = false; static final boolean DEBUG_DISPLAY = false; static final boolean DEBUG_POWER = false; diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 2ea1cf88447a..132e1ee99914 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -244,6 +244,22 @@ public abstract class WindowManagerInternal { public static abstract class AppTransitionListener { /** + * The display this listener is interested in. If it is INVALID_DISPLAY, then which display + * should be notified depends on the dispatcher. + */ + public final int mDisplayId; + + /** Let transition controller decide which display should receive the callbacks. */ + public AppTransitionListener() { + this(Display.INVALID_DISPLAY); + } + + /** It will listen the transition on the given display. */ + public AppTransitionListener(int displayId) { + mDisplayId = displayId; + } + + /** * Called when an app transition is being setup and about to be executed. */ public void onAppTransitionPendingLocked() {} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index acd8b3f1dbc3..f65eea0797bf 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -54,7 +54,6 @@ import static android.service.dreams.Flags.dreamHandlesConfirmKeys; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; -import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; @@ -97,6 +96,7 @@ import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN; +import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.window.WindowProviderService.isWindowProviderService; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; @@ -1070,7 +1070,6 @@ public class WindowManagerService extends IWindowManager.Stub /** Whether or not a layout can cause a wake up when theater mode is enabled. */ boolean mAllowTheaterModeWakeFromLayout; - final TaskPositioningController mTaskPositioningController; final DragDropController mDragDropController; /** For frozen screen animations. */ @@ -1428,7 +1427,6 @@ public class WindowManagerService extends IWindowManager.Stub mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromWindowLayout); - mTaskPositioningController = new TaskPositioningController(this); mDragDropController = new DragDropController(this, mH.getLooper()); mHighRefreshRateDenylist = HighRefreshRateDenylist.create(context.getResources()); @@ -9379,40 +9377,82 @@ public class WindowManagerService extends IWindowManager.Stub } /** - * Move focus to the adjacent embedded activity if the adjacent activity is more recently - * created or has a window more recently added. + * Returns the Activity that has the most recently created window in the adjacent activities + * if any. */ - boolean moveFocusToAdjacentEmbeddedWindow(@NonNull WindowState focusedWindow) { - final TaskFragment taskFragment = focusedWindow.getTaskFragment(); + @NonNull + ActivityRecord getMostRecentActivityInAdjacent(@NonNull ActivityRecord focusedActivity) { + final TaskFragment taskFragment = focusedActivity.getTaskFragment(); if (taskFragment == null) { - // Skip if not an Activity window. - return false; + // Return if activity no attached. + return focusedActivity; } if (!Flags.embeddedActivityBackNavFlag()) { - // Skip if flag is not enabled. - return false; + // Return if flag is not enabled. + return focusedActivity; } - if (!focusedWindow.mActivityRecord.isEmbedded()) { - // Skip if the focused activity is not embedded - return false; + if (!focusedActivity.isEmbedded()) { + // Return if the focused activity is not embedded. + return focusedActivity; } final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); final ActivityRecord adjacentTopActivity = adjacentTaskFragment != null ? adjacentTaskFragment.topRunningActivity() : null; if (adjacentTopActivity == null) { - return false; + // Return if no adjacent activity. + return focusedActivity; } if (adjacentTopActivity.getLastWindowCreateTime() - < focusedWindow.mActivityRecord.getLastWindowCreateTime()) { - // Skip if the current focus activity has more recently active window. + < focusedActivity.getLastWindowCreateTime()) { + // Return if the current focus activity has more recently active window. + return focusedActivity; + } + + return adjacentTopActivity; + } + + @NonNull + WindowState getMostRecentUsedEmbeddedWindowForBack(@NonNull WindowState focusedWindow) { + final ActivityRecord focusedActivity = focusedWindow.getActivityRecord(); + if (focusedActivity == null) { + // Not an Activity. + return focusedWindow; + } + + final ActivityRecord mostRecentActivityInAdjacent = getMostRecentActivityInAdjacent( + focusedActivity); + if (mostRecentActivityInAdjacent == focusedActivity) { + // Already be the most recent window. + return focusedWindow; + } + + // Looks for a candidate focused window on the adjacent Activity for the back event. + final WindowState candidate = + mostRecentActivityInAdjacent.getDisplayContent().findFocusedWindow( + mostRecentActivityInAdjacent); + return candidate != null ? candidate : focusedWindow; + } + + /** + * Move focus to the adjacent embedded activity if the adjacent activity is more recently + * created or has a window more recently added. + * <p> + * Returns {@code true} if the focused window is changed. Otherwise, returns {@code false}. + */ + boolean moveFocusToAdjacentEmbeddedWindow(@NonNull WindowState focusedWindow) { + final ActivityRecord activity = focusedWindow.getActivityRecord(); + if (activity == null) { return false; } - moveFocusToActivity(adjacentTopActivity); + final ActivityRecord mostRecentActivityInAdjacent = getMostRecentActivityInAdjacent( + activity); + + moveFocusToActivity(mostRecentActivityInAdjacent); return !focusedWindow.isFocused(); } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 60d3e787cac4..984caf1c692b 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED; import static android.os.Build.VERSION_CODES.Q; @@ -73,6 +74,7 @@ import android.os.LocaleList; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; @@ -112,6 +114,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio private static final String TAG_RELEASE = TAG + POSTFIX_RELEASE; private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION; + /** + * The max number of processes which can be top scheduling group if there are non-top visible + * freeform activities run in the process. + */ + private static final int MAX_NUM_PERCEPTIBLE_FREEFORM = + SystemProperties.getInt("persist.wm.max_num_perceptible_freeform", 1); + private static final int MAX_RAPID_ACTIVITY_LAUNCH_COUNT = 200; private static final long RAPID_ACTIVITY_LAUNCH_MS = 500; private static final long RESET_RAPID_ACTIVITY_LAUNCH_MS = 3 * RAPID_ACTIVITY_LAUNCH_MS; @@ -318,6 +327,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio public static final int ACTIVITY_STATE_FLAG_HAS_RESUMED = 1 << 21; public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22; public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23; + public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24; public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff; /** @@ -1229,6 +1239,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio ActivityRecord.State bestInvisibleState = DESTROYED; boolean allStoppingFinishing = true; boolean visible = false; + boolean hasResumedFreeform = false; int minTaskLayer = Integer.MAX_VALUE; int stateFlags = 0; final boolean wasResumed = hasResumedActivity(); @@ -1256,6 +1267,8 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio .processPriorityPolicyForMultiWindowMode() && task.getAdjacentTask() != null) { stateFlags |= ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN; + } else if (windowingMode == WINDOWING_MODE_FREEFORM) { + hasResumedFreeform = true; } } if (minTaskLayer > 0) { @@ -1289,6 +1302,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } + if (hasResumedFreeform + && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode() + // Exclude task layer 1 because it is already the top most. + && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) { + stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM; + } stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER; if (visible) { stateFlags |= ACTIVITY_STATE_FLAG_IS_VISIBLE; @@ -1655,6 +1674,22 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio // Otherwise if other places send wpc.getConfiguration() to client, the configuration may // be ignored due to the seq is older. resolvedConfig.seq = newParentConfig.seq; + + if (mConfigActivityRecord != null) { + // Let the activity decide whether to apply the size override. + return; + } + final DisplayContent displayContent = mAtm.mWindowManager != null + ? mAtm.mWindowManager.getDefaultDisplayContentLocked() + : null; + applySizeOverrideIfNeeded( + displayContent, + mInfo, + newParentConfig, + resolvedConfig, + false /* optsOutEdgeToEdge */, + false /* hasFixedRotationTransform */, + false /* hasCompatDisplayInsets */); } void dispatchConfiguration(@NonNull Configuration config) { @@ -2105,6 +2140,9 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio if ((stateFlags & ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN) != 0) { pw.print("RS|"); } + if ((stateFlags & ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) { + pw.print("PF|"); + } } } else if ((stateFlags & ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED) != 0) { pw.print("P|"); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 9ebb89dfe9b6..a36cff6d7bc5 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4648,14 +4648,16 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (!isImeLayeringTarget()) { return false; } - // Note that we don't process IME window if the IME input target is not on the screen. - // In case some unexpected IME visibility cases happen like starting the remote - // animation on the keyguard but seeing the IME window that originally on the app - // which behinds the keyguard. - final WindowState imeInputTarget = getImeInputTarget(); - if (imeInputTarget != null - && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { - return false; + if (!com.android.window.flags.Flags.doNotSkipImeByTargetVisibility()) { + // Note that we don't process IME window if the IME input target is not on the screen. + // In case some unexpected IME visibility cases happen like starting the remote + // animation on the keyguard but seeing the IME window that originally on the app + // which behinds the keyguard. + final WindowState imeInputTarget = getImeInputTarget(); + if (imeInputTarget != null + && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { + return false; + } } return mDisplayContent.forAllImeWindows(callback, traverseTopToBottom); } @@ -5504,7 +5506,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override public SurfaceControl getAnimationLeashParent() { - if (isStartingWindowAssociatedToTask()) { + if (mActivityRecord != null && !mActivityRecord.hasFixedRotationTransform() + && isStartingWindowAssociatedToTask()) { return mStartingData.mAssociatedTask.mSurfaceControl; } return super.getAnimationLeashParent(); diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index 21f7eca5627a..04d5c03e64a6 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -56,7 +56,10 @@ abstract class WindowTracing { static WindowTracing createDefaultAndStartLooper(WindowManagerService service, Choreographer choreographer) { - return new WindowTracingLegacy(service, choreographer); + if (!android.tracing.Flags.perfettoWmTracing()) { + return new WindowTracingLegacy(service, choreographer); + } + return new WindowTracingPerfetto(service, choreographer); } protected WindowTracing(WindowManagerService service, Choreographer choreographer, diff --git a/services/core/java/com/android/server/wm/WindowTracingDataSource.java b/services/core/java/com/android/server/wm/WindowTracingDataSource.java new file mode 100644 index 000000000000..3d2c0d3f79b9 --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTracingDataSource.java @@ -0,0 +1,204 @@ +/* + * 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.server.wm; + +import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT; + +import android.annotation.NonNull; +import android.internal.perfetto.protos.DataSourceConfigOuterClass.DataSourceConfig; +import android.internal.perfetto.protos.WindowmanagerConfig.WindowManagerConfig; +import android.tracing.perfetto.CreateTlsStateArgs; +import android.tracing.perfetto.DataSource; +import android.tracing.perfetto.DataSourceInstance; +import android.tracing.perfetto.DataSourceParams; +import android.tracing.perfetto.InitArguments; +import android.tracing.perfetto.Producer; +import android.tracing.perfetto.StartCallbackArguments; +import android.tracing.perfetto.StopCallbackArguments; +import android.util.Log; +import android.util.proto.ProtoInputStream; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public final class WindowTracingDataSource extends DataSource<WindowTracingDataSource.Instance, + WindowTracingDataSource.TlsState, Void> { + public static final String DATA_SOURCE_NAME = "android.windowmanager"; + + public static class TlsState { + public final Config mConfig; + public final AtomicBoolean mIsStarting = new AtomicBoolean(true); + + private TlsState(Config config) { + mConfig = config; + } + } + + public static class Config { + public final @WindowTraceLogLevel int mLogLevel; + public final boolean mLogOnFrame; + + private Config(@WindowTraceLogLevel int logLevel, boolean logOnFrame) { + mLogLevel = logLevel; + mLogOnFrame = logOnFrame; + } + } + + public abstract static class Instance extends DataSourceInstance { + public final Config mConfig; + + public Instance(DataSource dataSource, int instanceIndex, Config config) { + super(dataSource, instanceIndex); + mConfig = config; + } + } + + private static final Config CONFIG_DEFAULT = new Config(WindowTraceLogLevel.TRIM, true); + private static final int CONFIG_VALUE_UNSPECIFIED = 0; + private static final String TAG = "WindowTracingDataSource"; + + @NonNull + private final Consumer<Config> mOnStartCallback; + @NonNull + private final Consumer<Config> mOnStopCallback; + + public WindowTracingDataSource(@NonNull Consumer<Config> onStart, + @NonNull Consumer<Config> onStop) { + super(DATA_SOURCE_NAME); + mOnStartCallback = onStart; + mOnStopCallback = onStop; + + Producer.init(InitArguments.DEFAULTS); + DataSourceParams params = + new DataSourceParams.Builder() + .setBufferExhaustedPolicy( + PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT) + .build(); + register(params); + } + + @Override + public Instance createInstance(ProtoInputStream configStream, int instanceIndex) { + final Config config = parseDataSourceConfig(configStream); + + return new Instance(this, instanceIndex, config != null ? config : CONFIG_DEFAULT) { + @Override + protected void onStart(StartCallbackArguments args) { + mOnStartCallback.accept(mConfig); + } + + @Override + protected void onStop(StopCallbackArguments args) { + mOnStopCallback.accept(mConfig); + } + }; + } + + @Override + public TlsState createTlsState( + CreateTlsStateArgs<Instance> args) { + try (Instance dsInstance = args.getDataSourceInstanceLocked()) { + if (dsInstance == null) { + // Datasource instance has been removed + return new TlsState(CONFIG_DEFAULT); + } + return new TlsState(dsInstance.mConfig); + } + } + + private Config parseDataSourceConfig(ProtoInputStream stream) { + try { + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (stream.getFieldNumber() != (int) DataSourceConfig.WINDOWMANAGER_CONFIG) { + continue; + } + return parseWindowManagerConfig(stream); + } + Log.w(TAG, "Received start request without config parameters. Will use defaults."); + } catch (IOException e) { + throw new RuntimeException("Failed to parse DataSourceConfig", e); + } + return null; + } + + private Config parseWindowManagerConfig(ProtoInputStream stream) { + int parsedLogLevel = CONFIG_VALUE_UNSPECIFIED; + int parsedLogFrequency = CONFIG_VALUE_UNSPECIFIED; + + try { + final long token = stream.start(DataSourceConfig.WINDOWMANAGER_CONFIG); + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) WindowManagerConfig.LOG_LEVEL: + parsedLogLevel = stream.readInt(WindowManagerConfig.LOG_LEVEL); + break; + case (int) WindowManagerConfig.LOG_FREQUENCY: + parsedLogFrequency = stream.readInt(WindowManagerConfig.LOG_FREQUENCY); + break; + default: + Log.w(TAG, "Unrecognized WindowManagerConfig field number: " + + stream.getFieldNumber()); + } + } + stream.end(token); + } catch (IOException e) { + throw new RuntimeException("Failed to parse WindowManagerConfig", e); + } + + @WindowTraceLogLevel int logLevel; + switch(parsedLogLevel) { + case CONFIG_VALUE_UNSPECIFIED: + Log.w(TAG, "Unspecified log level. Defaulting to TRIM"); + logLevel = WindowTraceLogLevel.TRIM; + break; + case WindowManagerConfig.LOG_LEVEL_VERBOSE: + logLevel = WindowTraceLogLevel.ALL; + break; + case WindowManagerConfig.LOG_LEVEL_DEBUG: + logLevel = WindowTraceLogLevel.TRIM; + break; + case WindowManagerConfig.LOG_LEVEL_CRITICAL: + logLevel = WindowTraceLogLevel.CRITICAL; + break; + default: + Log.w(TAG, "Unrecognized log level. Defaulting to TRIM"); + logLevel = WindowTraceLogLevel.TRIM; + break; + } + + boolean logOnFrame; + switch(parsedLogFrequency) { + case CONFIG_VALUE_UNSPECIFIED: + Log.w(TAG, "Unspecified log frequency. Defaulting to 'log on frame'"); + logOnFrame = true; + break; + case WindowManagerConfig.LOG_FREQUENCY_FRAME: + logOnFrame = true; + break; + case WindowManagerConfig.LOG_FREQUENCY_TRANSACTION: + logOnFrame = false; + break; + default: + Log.w(TAG, "Unrecognized log frequency. Defaulting to 'log on frame'"); + logOnFrame = true; + break; + } + + return new Config(logLevel, logOnFrame); + } +} diff --git a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java new file mode 100644 index 000000000000..653b6dac1537 --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java @@ -0,0 +1,163 @@ +/* + * 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.server.wm; + +import android.annotation.Nullable; +import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket; +import android.internal.perfetto.protos.WinscopeExtensionsImplOuterClass.WinscopeExtensionsImpl; +import android.os.ShellCommand; +import android.os.SystemClock; +import android.util.Log; +import android.util.proto.ProtoOutputStream; +import android.view.Choreographer; + +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicInteger; + +class WindowTracingPerfetto extends WindowTracing { + private static final String TAG = "WindowTracing"; + + private final AtomicInteger mCountSessionsOnFrame = new AtomicInteger(); + private final AtomicInteger mCountSessionsOnTransaction = new AtomicInteger(); + private final WindowTracingDataSource mDataSource = new WindowTracingDataSource( + this::onStart, this::onStop); + + WindowTracingPerfetto(WindowManagerService service, Choreographer choreographer) { + super(service, choreographer, service.mGlobalLock); + } + + @Override + void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) { + logAndPrintln(pw, "Log level must be configured through perfetto"); + } + + @Override + void setLogFrequency(boolean onFrame, PrintWriter pw) { + logAndPrintln(pw, "Log frequency must be configured through perfetto"); + } + + @Override + void setBufferCapacity(int capacity, PrintWriter pw) { + logAndPrintln(pw, "Buffer capacity must be configured through perfetto"); + } + + @Override + boolean isEnabled() { + return (mCountSessionsOnFrame.get() + mCountSessionsOnTransaction.get()) > 0; + } + + @Override + int onShellCommand(ShellCommand shell) { + PrintWriter pw = shell.getOutPrintWriter(); + pw.println("Shell commands are ignored." + + " Any type of action should be performed through perfetto."); + return -1; + } + + @Override + String getStatus() { + return "Status: " + + ((isEnabled()) ? "Enabled" : "Disabled") + + "\n" + + "Sessions logging 'on frame': " + mCountSessionsOnFrame.get() + + "\n" + + "Sessions logging 'on transaction': " + mCountSessionsOnTransaction.get() + + "\n"; + } + + @Override + protected void startTraceInternal(@Nullable PrintWriter pw) { + logAndPrintln(pw, "Tracing must be started through perfetto"); + } + + @Override + protected void stopTraceInternal(@Nullable PrintWriter pw) { + logAndPrintln(pw, "Tracing must be stopped through perfetto"); + } + + @Override + protected void saveForBugreportInternal(@Nullable PrintWriter pw) { + logAndPrintln(pw, "Tracing snapshot for bugreport must be handled through perfetto"); + } + + @Override + protected void log(String where) { + try { + boolean isStartLogEvent = where == WHERE_START_TRACING; + boolean isOnFrameLogEvent = where == WHERE_ON_FRAME; + + mDataSource.trace((context) -> { + WindowTracingDataSource.Config dataSourceConfig = + context.getCustomTlsState().mConfig; + + if (isStartLogEvent) { + boolean isDataSourceStarting = context.getCustomTlsState() + .mIsStarting.compareAndSet(true, false); + if (!isDataSourceStarting) { + return; + } + } else if (isOnFrameLogEvent != dataSourceConfig.mLogOnFrame) { + return; + } + + ProtoOutputStream os = context.newTracePacket(); + long timestamp = SystemClock.elapsedRealtimeNanos(); + os.write(TracePacket.TIMESTAMP, timestamp); + final long tokenWinscopeExtensions = + os.start(TracePacket.WINSCOPE_EXTENSIONS); + final long tokenExtensionsField = + os.start(WinscopeExtensionsImpl.WINDOWMANAGER); + dumpToProto(os, dataSourceConfig.mLogLevel, where, timestamp); + os.end(tokenExtensionsField); + os.end(tokenWinscopeExtensions); + }); + } catch (Exception e) { + Log.wtf(TAG, "Exception while tracing state", e); + } + } + + @Override + protected boolean shouldLogOnFrame() { + return mCountSessionsOnFrame.get() > 0; + } + + @Override + protected boolean shouldLogOnTransaction() { + return mCountSessionsOnTransaction.get() > 0; + } + + private void onStart(WindowTracingDataSource.Config config) { + if (config.mLogOnFrame) { + mCountSessionsOnFrame.incrementAndGet(); + } else { + mCountSessionsOnTransaction.incrementAndGet(); + } + + Log.i(TAG, "Started with logLevel: " + config.mLogLevel + + " logOnFrame: " + config.mLogOnFrame); + log(WHERE_START_TRACING); + } + + private void onStop(WindowTracingDataSource.Config config) { + if (config.mLogOnFrame) { + mCountSessionsOnFrame.decrementAndGet(); + } else { + mCountSessionsOnTransaction.decrementAndGet(); + } + Log.i(TAG, "Stopped"); + } +} diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java index 4211764085b1..3559e620a350 100644 --- a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java +++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java @@ -47,16 +47,13 @@ public enum DesktopModeFlagsUtil { Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true); private static final String TAG = "DesktopModeFlagsUtil"; - private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = - "sys.wmshell.desktopmode.dev_toggle_override"; - // Function called to obtain aconfig flag value. private final Supplier<Boolean> mFlagFunction; // Whether the flag state should be affected by developer option. private final boolean mShouldOverrideByDevOption; // Local cache for toggle override, which is initialized once on its first access. It needs to - // be refreshed only on reboots as overridden state takes effect on reboots. + // be refreshed only on reboots as overridden state is expected to take effect on reboots. private static ToggleOverride sCachedToggleOverride; DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) { @@ -67,9 +64,6 @@ public enum DesktopModeFlagsUtil { /** * Determines state of flag based on the actual flag and desktop mode developer option * overrides. - * - * Note: this method makes sure that a constant developer toggle overrides is read until - * reboot. */ public boolean isEnabled(Context context) { if (!Flags.showDesktopWindowingDevOption() @@ -102,49 +96,15 @@ public enum DesktopModeFlagsUtil { } /** - * Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise - * initializes the system property by reading Settings.Global. + * Returns {@link ToggleOverride} from Settings.Global set by toggle. */ private ToggleOverride getToggleOverrideFromSystem(Context context) { - // A non-persistent System Property is used to store override to ensure it remains - // constant till reboot. - String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null); - ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty); - - // If valid system property, return it - if (overrideFromSystemProperties != null) { - return overrideFromSystemProperties; - } - - // Fallback when System Property is not present (just after reboot) or not valid (user - // manually changed the value): Read from Settings.Global int settingValue = Settings.Global.getInt( context.getContentResolver(), Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, OVERRIDE_UNSET.getSetting() ); - ToggleOverride overrideFromSettingsGlobal = - ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET); - // Initialize System Property - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue)); - return overrideFromSettingsGlobal; - } - - /** - * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if - * {@code intString} does not correspond to a {@link ToggleOverride}. - */ - private static @Nullable ToggleOverride convertToToggleOverride( - @Nullable String intString - ) { - if (intString == null) return null; - try { - int intValue = Integer.parseInt(intString); - return ToggleOverride.fromSetting(intValue, null); - } catch (NumberFormatException e) { - Log.w(TAG, "Unknown toggleOverride int " + intString); - return null; - } + return ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET); } /** Override state of desktop mode developer option toggle. */ diff --git a/services/core/jni/com_android_server_UsbDeviceManager.cpp b/services/core/jni/com_android_server_UsbDeviceManager.cpp index 9dc70afad9d9..4ef9cf4d2388 100644 --- a/services/core/jni/com_android_server_UsbDeviceManager.cpp +++ b/services/core/jni/com_android_server_UsbDeviceManager.cpp @@ -15,33 +15,168 @@ */ #define LOG_TAG "UsbDeviceManagerJNI" -#include "utils/Log.h" - -#include "jni.h" +#include <android-base/properties.h> +#include <android-base/unique_fd.h> +#include <core_jni_helpers.h> +#include <fcntl.h> +#include <linux/usb/f_accessory.h> #include <nativehelper/JNIPlatformHelp.h> #include <nativehelper/ScopedUtfChars.h> -#include "android_runtime/AndroidRuntime.h" -#include "android_runtime/Log.h" -#include "MtpDescriptors.h" - #include <stdio.h> -#include <sys/types.h> -#include <sys/stat.h> -#include <fcntl.h> +#include <sys/epoll.h> #include <sys/ioctl.h> -#include <linux/usb/f_accessory.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <thread> + +#include "MtpDescriptors.h" +#include "android_runtime/AndroidRuntime.h" +#include "android_runtime/Log.h" +#include "jni.h" +#include "utils/Log.h" #define DRIVER_NAME "/dev/usb_accessory" +#define EPOLL_MAX_EVENTS 4 +#define USB_STATE_MAX_LEN 20 namespace android { +static JavaVM *gvm = nullptr; +static jmethodID gUpdateGadgetStateMethod; + static struct parcel_file_descriptor_offsets_t { jclass mClass; jmethodID mConstructor; } gParcelFileDescriptorOffsets; +/* + * NativeGadgetMonitorThread starts a new thread to monitor udc state by epoll, + * convert and update the state to UsbDeviceManager. + */ +class NativeGadgetMonitorThread { + android::base::unique_fd mMonitorFd; + int mPipefd[2]; + std::thread mThread; + jobject mCallbackObj; + std::string mGadgetState; + + void handleStateUpdate(const char *state) { + JNIEnv *env = AndroidRuntime::getJNIEnv(); + std::string gadgetState; + + if (!std::strcmp(state, "not attached\n")) { + gadgetState = "DISCONNECTED"; + } else if (!std::strcmp(state, "attached\n") || !std::strcmp(state, "powered\n") || + !std::strcmp(state, "default\n") || !std::strcmp(state, "addressed\n")) { + gadgetState = "CONNECTED"; + } else if (!std::strcmp(state, "configured\n")) { + gadgetState = "CONFIGURED"; + } else if (!std::strcmp(state, "suspended\n")) { + return; + } else { + ALOGE("Unknown gadget state %s", state); + return; + } + + if (mGadgetState.compare(gadgetState)) { + mGadgetState = gadgetState; + jstring obj = env->NewStringUTF(gadgetState.c_str()); + env->CallVoidMethod(mCallbackObj, gUpdateGadgetStateMethod, obj); + } + } + + int setupEpoll(android::base::unique_fd &epollFd) { + struct epoll_event ev; + + ev.data.fd = mMonitorFd.get(); + ev.events = EPOLLPRI; + if (epoll_ctl(epollFd.get(), EPOLL_CTL_ADD, mMonitorFd.get(), &ev) != 0) { + ALOGE("epoll_ctl failed for monitor fd; errno=%d", errno); + return errno; + } + + ev.data.fd = mPipefd[0]; + ev.events = EPOLLIN; + if (epoll_ctl(epollFd.get(), EPOLL_CTL_ADD, mPipefd[0], &ev) != 0) { + ALOGE("epoll_ctl failed for pipe fd; errno=%d", errno); + return errno; + } + + return 0; + } + + void monitorLoop() { + android::base::unique_fd epollFd(epoll_create(EPOLL_MAX_EVENTS)); + if (epollFd.get() == -1) { + ALOGE("epoll_create failed; errno=%d", errno); + return; + } + if (setupEpoll(epollFd) != 0) return; + + JNIEnv *env = nullptr; + JavaVMAttachArgs aargs = {JNI_VERSION_1_4, "NativeGadgetMonitorThread", nullptr}; + if (gvm->AttachCurrentThread(&env, &aargs) != JNI_OK || env == nullptr) { + ALOGE("Couldn't attach thread"); + return; + } + + struct epoll_event events[EPOLL_MAX_EVENTS]; + int nevents = 0; + while (true) { + nevents = epoll_wait(epollFd.get(), events, EPOLL_MAX_EVENTS, -1); + if (nevents < 0) { + ALOGE("usb epoll_wait failed; errno=%d", errno); + continue; + } + for (int i = 0; i < nevents; ++i) { + int fd = events[i].data.fd; + if (fd == mPipefd[0]) { + goto exit; + } else if (fd == mMonitorFd.get()) { + char state[USB_STATE_MAX_LEN] = {0}; + lseek(fd, 0, SEEK_SET); + read(fd, &state, USB_STATE_MAX_LEN); + handleStateUpdate(state); + } + } + } + + exit: + auto res = gvm->DetachCurrentThread(); + ALOGE_IF(res != JNI_OK, "Couldn't detach thread"); + return; + } + + void stop() { + if (mThread.joinable()) { + int c = 'q'; + write(mPipefd[1], &c, 1); + mThread.join(); + } + } + + DISALLOW_COPY_AND_ASSIGN(NativeGadgetMonitorThread); + +public: + explicit NativeGadgetMonitorThread(jobject obj, android::base::unique_fd monitorFd) + : mMonitorFd(std::move(monitorFd)), mGadgetState("") { + mCallbackObj = AndroidRuntime::getJNIEnv()->NewGlobalRef(obj); + pipe(mPipefd); + mThread = std::thread(&NativeGadgetMonitorThread::monitorLoop, this); + } + + ~NativeGadgetMonitorThread() { + stop(); + close(mPipefd[0]); + close(mPipefd[1]); + AndroidRuntime::getJNIEnv()->DeleteGlobalRef(mCallbackObj); + } +}; +static std::unique_ptr<NativeGadgetMonitorThread> sGadgetMonitorThread; + static void set_accessory_string(JNIEnv *env, int fd, int cmd, jobjectArray strArray, int index) { char buffer[256]; @@ -135,6 +270,41 @@ static jobject android_server_UsbDeviceManager_openControl(JNIEnv *env, jobject return jifd; } +static jboolean android_server_UsbDeviceManager_startGadgetMonitor(JNIEnv *env, jobject thiz, + jstring jUdcName) { + std::string filePath; + ScopedUtfChars udcName(env, jUdcName); + + filePath = "/sys/class/udc/" + std::string(udcName.c_str()) + "/state"; + android::base::unique_fd fd(open(filePath.c_str(), O_RDONLY)); + + if (fd.get() == -1) { + ALOGE("Cannot open %s", filePath.c_str()); + return JNI_FALSE; + } + + ALOGI("Start monitoring %s", filePath.c_str()); + sGadgetMonitorThread.reset(new NativeGadgetMonitorThread(thiz, std::move(fd))); + + return JNI_TRUE; +} + +static void android_server_UsbDeviceManager_stopGadgetMonitor(JNIEnv *env, jobject /* thiz */) { + sGadgetMonitorThread.reset(); + return; +} + +static jstring android_server_UsbDeviceManager_waitAndGetProperty(JNIEnv *env, jobject thiz, + jstring jPropName) { + ScopedUtfChars propName(env, jPropName); + std::string propValue; + + while (!android::base::WaitForPropertyCreation(propName.c_str())); + propValue = android::base::GetProperty(propName.c_str(), "" /* default */); + + return env->NewStringUTF(propValue.c_str()); +} + static const JNINativeMethod method_table[] = { {"nativeGetAccessoryStrings", "()[Ljava/lang/String;", (void *)android_server_UsbDeviceManager_getAccessoryStrings}, @@ -143,16 +313,26 @@ static const JNINativeMethod method_table[] = { {"nativeIsStartRequested", "()Z", (void *)android_server_UsbDeviceManager_isStartRequested}, {"nativeOpenControl", "(Ljava/lang/String;)Ljava/io/FileDescriptor;", (void *)android_server_UsbDeviceManager_openControl}, + {"nativeStartGadgetMonitor", "(Ljava/lang/String;)Z", + (void *)android_server_UsbDeviceManager_startGadgetMonitor}, + {"nativeStopGadgetMonitor", "()V", + (void *)android_server_UsbDeviceManager_stopGadgetMonitor}, + {"nativeWaitAndGetProperty", "(Ljava/lang/String;)Ljava/lang/String;", + (void *)android_server_UsbDeviceManager_waitAndGetProperty}, }; -int register_android_server_UsbDeviceManager(JNIEnv *env) -{ +int register_android_server_UsbDeviceManager(JavaVM *vm, JNIEnv *env) { + gvm = vm; + jclass clazz = env->FindClass("com/android/server/usb/UsbDeviceManager"); if (clazz == NULL) { ALOGE("Can't find com/android/server/usb/UsbDeviceManager"); return -1; } + gUpdateGadgetStateMethod = + GetMethodIDOrDie(env, clazz, "updateGadgetState", "(Ljava/lang/String;)V"); + clazz = env->FindClass("android/os/ParcelFileDescriptor"); LOG_FATAL_IF(clazz == NULL, "Unable to find class android.os.ParcelFileDescriptor"); gParcelFileDescriptorOffsets.mClass = (jclass) env->NewGlobalRef(clazz); @@ -163,5 +343,4 @@ int register_android_server_UsbDeviceManager(JNIEnv *env) return jniRegisterNativeMethods(env, "com/android/server/usb/UsbDeviceManager", method_table, NELEM(method_table)); } - }; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 5719810abc5a..4d6a90c81b27 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -104,7 +104,6 @@ static const char* VELOCITYTRACKER_STRATEGY = "velocitytracker_strategy"; static struct { jclass clazz; - jmethodID notifyConfigurationChanged; jmethodID notifyInputDevicesChanged; jmethodID notifySwitch; jmethodID notifyInputChannelBroken; @@ -314,7 +313,6 @@ public: void getReaderConfiguration(InputReaderConfiguration* outConfig) override; void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override; - void notifyConfigurationChanged(nsecs_t when) override; std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay( const InputDeviceIdentifier& identifier, const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override; @@ -940,18 +938,6 @@ void NativeInputManager::notifySwitch(nsecs_t when, checkAndClearExceptionFromCallback(env, "notifySwitch"); } -void NativeInputManager::notifyConfigurationChanged(nsecs_t when) { -#if DEBUG_INPUT_DISPATCHER_POLICY - ALOGD("notifyConfigurationChanged - when=%lld", when); -#endif - ATRACE_CALL(); - - JNIEnv* env = jniEnv(); - - env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyConfigurationChanged, when); - checkAndClearExceptionFromCallback(env, "notifyConfigurationChanged"); -} - static jobject getInputApplicationHandleObjLocalRef( JNIEnv* env, const std::shared_ptr<InputApplicationHandle>& inputApplicationHandle) { if (inputApplicationHandle == nullptr) { @@ -2873,9 +2859,6 @@ int register_android_server_InputManager(JNIEnv* env) { FIND_CLASS(clazz, "com/android/server/input/InputManagerService"); gServiceClassInfo.clazz = reinterpret_cast<jclass>(env->NewGlobalRef(clazz)); - GET_METHOD_ID(gServiceClassInfo.notifyConfigurationChanged, clazz, - "notifyConfigurationChanged", "(J)V"); - GET_METHOD_ID(gServiceClassInfo.notifyInputDevicesChanged, clazz, "notifyInputDevicesChanged", "([Landroid/view/InputDevice;)V"); diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 6464081d615a..314ff9d3c808 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -37,7 +37,7 @@ int register_android_server_SerialService(JNIEnv* env); int register_android_server_SystemServer(JNIEnv* env); int register_android_server_UsbAlsaJackDetector(JNIEnv* env); int register_android_server_UsbAlsaMidiDevice(JNIEnv* env); -int register_android_server_UsbDeviceManager(JNIEnv* env); +int register_android_server_UsbDeviceManager(JavaVM* vm, JNIEnv* env); int register_android_server_UsbHostManager(JNIEnv* env); int register_android_server_vr_VrManagerService(JNIEnv* env); int register_android_server_vibrator_VibratorController(JavaVM* vm, JNIEnv* env); @@ -96,7 +96,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_SerialService(env); register_android_server_InputManager(env); register_android_server_LightsService(env); - register_android_server_UsbDeviceManager(env); + register_android_server_UsbDeviceManager(vm, env); register_android_server_UsbAlsaJackDetector(env); register_android_server_UsbAlsaMidiDevice(env); register_android_server_UsbHostManager(env); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index dc8cec91001b..6a0dd5a04f82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -182,6 +182,7 @@ class ActiveAdmin { private static final String TAG_CREDENTIAL_MANAGER_POLICY = "credential-manager-policy"; private static final String TAG_DIALER_PACKAGE = "dialer_package"; private static final String TAG_SMS_PACKAGE = "sms_package"; + private static final String TAG_PROVISIONING_CONTEXT = "provisioning-context"; // If the ActiveAdmin is a permission-based admin, then info will be null because the // permission-based admin is not mapped to a device administrator component. @@ -359,6 +360,8 @@ class ActiveAdmin { int mWifiMinimumSecurityLevel = DevicePolicyManager.WIFI_SECURITY_OPEN; String mDialerPackage; String mSmsPackage; + private String mProvisioningContext; + private static final int PROVISIONING_CONTEXT_LENGTH_LIMIT = 1000; ActiveAdmin(DeviceAdminInfo info, boolean isParent) { this.userId = -1; @@ -404,6 +407,23 @@ class ActiveAdmin { return UserHandle.of(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid)); } + /** + * Stores metadata about context of setting an active admin + * @param provisioningContext some metadata, for example test method name + */ + public void setProvisioningContext(@Nullable String provisioningContext) { + if (Flags.provisioningContextParameter() + && !TextUtils.isEmpty(provisioningContext) + && !provisioningContext.isBlank()) { + if (provisioningContext.length() > PROVISIONING_CONTEXT_LENGTH_LIMIT) { + mProvisioningContext = provisioningContext.substring( + 0, PROVISIONING_CONTEXT_LENGTH_LIMIT); + } else { + mProvisioningContext = provisioningContext; + } + } + } + void writeToXml(TypedXmlSerializer out) throws IllegalArgumentException, IllegalStateException, IOException { if (info != null) { @@ -694,6 +714,12 @@ class ActiveAdmin { if (!TextUtils.isEmpty(mSmsPackage)) { writeAttributeValueToXml(out, TAG_SMS_PACKAGE, mSmsPackage); } + + if (Flags.provisioningContextParameter() && !TextUtils.isEmpty(mProvisioningContext)) { + out.startTag(null, TAG_PROVISIONING_CONTEXT); + out.attribute(null, ATTR_VALUE, mProvisioningContext); + out.endTag(null, TAG_PROVISIONING_CONTEXT); + } } private void writePackagePolicy(TypedXmlSerializer out, String tag, @@ -1006,6 +1032,9 @@ class ActiveAdmin { mDialerPackage = parser.getAttributeValue(null, ATTR_VALUE); } else if (TAG_SMS_PACKAGE.equals(tag)) { mSmsPackage = parser.getAttributeValue(null, ATTR_VALUE); + } else if (Flags.provisioningContextParameter() + && TAG_PROVISIONING_CONTEXT.equals(tag)) { + mProvisioningContext = parser.getAttributeValue(null, ATTR_VALUE); } else { Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag); XmlUtils.skipCurrentTag(parser); @@ -1496,5 +1525,10 @@ class ActiveAdmin { pw.println(mDialerPackage); pw.print("mSmsPackage="); pw.println(mSmsPackage); + + if (Flags.provisioningContextParameter()) { + pw.print("mProvisioningContext="); + pw.println(mProvisioningContext); + } } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 032d6b56af1b..7cb8ace697ec 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1378,7 +1378,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Clear always-on configuration if it wasn't set by the admin. if (adminConfiguredVpnPkg == null) { - mInjector.getVpnManager().setAlwaysOnVpnPackageForUser(userId, null, false, null); + VpnManager vpnManager = mInjector.getVpnManager(); + if (vpnManager != null) { + vpnManager.setAlwaysOnVpnPackageForUser(userId, null, false, null); + } } // Clear app authorizations to establish VPNs. When DISALLOW_CONFIG_VPN is enforced apps @@ -1789,6 +1792,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return mContext.getSystemService(ConnectivityManager.class); } + @Nullable VpnManager getVpnManager() { return mContext.getSystemService(VpnManager.class); } @@ -3943,10 +3947,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { /** * @param adminReceiver The admin to add * @param refreshing true = update an active admin, no error + * @param userHandle which user this admin will be set on + * @param provisioningContext additional information for debugging */ @Override public void setActiveAdmin( - ComponentName adminReceiver, boolean refreshing, int userHandle) { + ComponentName adminReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { if (!mHasFeature) { return; } @@ -3972,6 +3982,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { newAdmin.testOnlyAdmin = (existingAdmin != null) ? existingAdmin.testOnlyAdmin : isPackageTestOnly(adminReceiver.getPackageName(), userHandle); + newAdmin.setProvisioningContext(provisioningContext); policy.mAdminMap.put(adminReceiver, newAdmin); int replaceIndex = -1; final int N = policy.mAdminList.size(); @@ -7697,8 +7708,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } // If some package is uninstalled after the check above, it will be ignored by CM. - if (!mInjector.getVpnManager().setAlwaysOnVpnPackageForUser( - userId, vpnPackage, lockdown, lockdownAllowlist)) { + VpnManager vpnManager = mInjector.getVpnManager(); + if (vpnManager == null + || !mInjector.getVpnManager().setAlwaysOnVpnPackageForUser( + userId, vpnPackage, lockdown, lockdownAllowlist)) { throw new UnsupportedOperationException(); } }); @@ -7746,8 +7759,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization( isDefaultDeviceOwner(caller) || isProfileOwner(caller)); + VpnManager vpnManager = mInjector.getVpnManager(); + if (vpnManager == null) { + return null; + } return mInjector.binderWithCleanCallingIdentity( - () -> mInjector.getVpnManager().getAlwaysOnVpnPackageForUser(caller.getUserId())); + () -> vpnManager.getAlwaysOnVpnPackageForUser(caller.getUserId())); } @Override @@ -7774,8 +7791,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { isDefaultDeviceOwner(caller) || isProfileOwner(caller)); } + VpnManager vpnManager = mInjector.getVpnManager(); + if (vpnManager == null) { + return false; + } return mInjector.binderWithCleanCallingIdentity( - () -> mInjector.getVpnManager().isVpnLockdownEnabled(caller.getUserId())); + () -> vpnManager.isVpnLockdownEnabled(caller.getUserId())); } @Override @@ -7797,8 +7818,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization( isDefaultDeviceOwner(caller) || isProfileOwner(caller)); + VpnManager vpnManager = mInjector.getVpnManager(); + if (vpnManager == null) { + return null; + } return mInjector.binderWithCleanCallingIdentity( - () -> mInjector.getVpnManager().getVpnLockdownAllowlist(caller.getUserId())); + () -> vpnManager.getVpnLockdownAllowlist(caller.getUserId())); } private void forceWipeDeviceNoLock(boolean wipeExtRequested, String reason, boolean wipeEuicc, @@ -12830,7 +12855,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { }); // Set admin. - setActiveAdmin(profileOwner, /* refreshing= */ true, userId); + setActiveAdmin(profileOwner, /* refreshing= */ true, userId, null); setProfileOwner(profileOwner, userId); synchronized (getLockObject()) { @@ -21883,7 +21908,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @UserIdInt int userId, @UserIdInt int callingUserId, ComponentName adminComponent) { final String adminPackage = adminComponent.getPackageName(); enablePackage(adminPackage, callingUserId); - setActiveAdmin(adminComponent, /* refreshing= */ true, userId); + setActiveAdmin(adminComponent, /* refreshing= */ true, userId, null); } private void enablePackage(String packageName, @UserIdInt int userId) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java index eb893fcfee1f..0cd5b47b75c0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.ComponentName; import android.os.ShellCommand; import android.os.SystemClock; @@ -46,11 +47,13 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private static final String USER_OPTION = "--user"; private static final String DO_ONLY_OPTION = "--device-owner-only"; + private static final String PROVISIONING_CONTEXT_OPTION = "--provisioning-context"; private final DevicePolicyManagerService mService; private int mUserId = UserHandle.USER_SYSTEM; private ComponentName mComponent; private boolean mSetDoOnly; + private String mProvisioningContext = null; DevicePolicyManagerServiceShellCommand(DevicePolicyManagerService service) { mService = Objects.requireNonNull(service); @@ -127,15 +130,28 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { pw.printf(" Lists the device / profile owners per user \n\n"); pw.printf(" %s\n", CMD_LIST_POLICY_EXEMPT_APPS); pw.printf(" Lists the apps that are exempt from policies\n\n"); - pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", - CMD_SET_ACTIVE_ADMIN, USER_OPTION); - pw.printf(" Sets the given component as active admin for an existing user.\n\n"); - pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" - + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION); - pw.printf(" Sets the given component as active admin, and its package as device owner." - + "\n\n"); - pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", - CMD_SET_PROFILE_OWNER, USER_OPTION); + if (Flags.provisioningContextParameter()) { + pw.printf(" %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_ACTIVE_ADMIN, USER_OPTION, PROVISIONING_CONTEXT_OPTION); + pw.printf(" Sets the given component as active admin for an existing user.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" + + " [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION, PROVISIONING_CONTEXT_OPTION); + pw.printf(" Sets the given component as active admin, and its package as device" + + " owner.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_PROFILE_OWNER, USER_OPTION, PROVISIONING_CONTEXT_OPTION); + } else { + pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", + CMD_SET_ACTIVE_ADMIN, USER_OPTION); + pw.printf(" Sets the given component as active admin for an existing user.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" + + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION); + pw.printf(" Sets the given component as active admin, and its package as device" + + " owner.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", + CMD_SET_PROFILE_OWNER, USER_OPTION); + } pw.printf(" Sets the given component as active admin and profile owner for an existing " + "user.\n\n"); pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", @@ -243,7 +259,7 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private int runSetActiveAdmin(PrintWriter pw) { parseArgs(); - mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId); + mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext); pw.printf("Success: Active admin set to component %s\n", mComponent.flattenToShortString()); return 0; @@ -253,7 +269,12 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { parseArgs(); boolean isAdminAdded = false; try { - mService.setActiveAdmin(mComponent, /* refreshing= */ false, mUserId); + mService.setActiveAdmin( + mComponent, + /* refreshing= */ false, + mUserId, + mProvisioningContext + ); isAdminAdded = true; } catch (IllegalArgumentException e) { pw.printf("%s was already an admin for user %d. No need to set it again.\n", @@ -291,7 +312,7 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private int runSetProfileOwner(PrintWriter pw) { parseArgs(); - mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId); + mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext); try { if (!mService.setProfileOwner(mComponent, mUserId)) { @@ -363,6 +384,8 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { } } else if (DO_ONLY_OPTION.equals(opt)) { mSetDoOnly = true; + } else if (PROVISIONING_CONTEXT_OPTION.equals(opt)) { + mProvisioningContext = getNextArgRequired(); } else { throw new IllegalArgumentException("Unknown option: " + opt); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java index cbd28475bc26..6e038f9b67a0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.VerifierDeviceIdentity; import android.net.wifi.WifiManager; import android.os.Build; @@ -77,13 +78,14 @@ class EnterpriseSpecificIdCalculator { mMeid = meid; mSerialNumber = Build.getSerial(); WifiManager wifiManager = context.getSystemService(WifiManager.class); - Preconditions.checkState(wifiManager != null, "Unable to access WiFi service"); - final String[] macAddresses = wifiManager.getFactoryMacAddresses(); - if (macAddresses == null || macAddresses.length == 0) { - mMacAddress = ""; - } else { - mMacAddress = macAddresses[0]; + String macAddress = ""; + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { + final String[] macAddresses = wifiManager.getFactoryMacAddresses(); + if (macAddresses != null && macAddresses.length > 0) { + macAddress = macAddresses[0]; + } } + mMacAddress = macAddress; } private static String getPaddedTruncatedString(String input, int maxLength) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index db4b171152a7..9e8811f419a2 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -153,6 +153,7 @@ import com.android.server.contentsuggestions.ContentSuggestionsManagerService; import com.android.server.contextualsearch.ContextualSearchManagerService; import com.android.server.coverage.CoverageService; import com.android.server.cpu.CpuMonitorService; +import com.android.server.crashrecovery.CrashRecoveryModule; import com.android.server.credentials.CredentialManagerService; import com.android.server.criticalevents.CriticalEventLog; import com.android.server.devicepolicy.DevicePolicyManagerService; @@ -381,8 +382,6 @@ public final class SystemServer implements Dumpable { + "OnDevicePersonalizationSystemService$Lifecycle"; private static final String UPDATABLE_DEVICE_CONFIG_SERVICE_CLASS = "com.android.server.deviceconfig.DeviceConfigInit$Lifecycle"; - private static final String CRASHRECOVERY_MODULE_LIFECYCLE_CLASS = - "com.android.server.crashrecovery.CrashRecoveryModule$Lifecycle"; /* @@ -2939,7 +2938,7 @@ public final class SystemServer implements Dumpable { if (Flags.refactorCrashrecovery()) { t.traceBegin("StartCrashRecoveryModule"); - mSystemServiceManager.startService(CRASHRECOVERY_MODULE_LIFECYCLE_CLASS); + mSystemServiceManager.startService(CrashRecoveryModule.Lifecycle.class); t.traceEnd(); } else { if (Flags.recoverabilityDetection()) { diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index 3ed6ad78343b..acdbbdee7d67 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -398,13 +398,13 @@ public final class ProfcollectForwardingService extends SystemService { if (randomNum >= traceFrequency) { return; } - // For a small percentage a traces, we collect the initialization behavior. - boolean traceInitialization = ThreadLocalRandom.current().nextInt(10) < 1; - int traceDelay = traceInitialization ? 0 : 1000; - String traceTag = traceInitialization ? "camera_init" : "camera"; + final int traceDelay = 1000; + final int traceDuration = 5000; + final String traceTag = "camera"; BackgroundThread.get().getThreadHandler().postDelayed(() -> { try { - mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider"); + mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider", + traceDuration); } catch (RemoteException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java index 63224bb2aa3f..c54ff5fb2f69 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java @@ -25,7 +25,7 @@ import android.util.AtomicFile; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; @@ -54,7 +54,8 @@ public final class AdditionalSubtypeUtilsTest { // Save & load. AtomicFile atomicFile = new AtomicFile( - new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml")); + new File(InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(), + "subtypes.xml")); AdditionalSubtypeUtils.saveToFile(AdditionalSubtypeMap.of(allSubtypes), InputMethodMap.of(methodMap), atomicFile); AdditionalSubtypeMap loadedSubtypes = AdditionalSubtypeUtils.loadFromFile(atomicFile); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index 267ce26515d7..ec9bfa7200c6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -59,6 +59,7 @@ import android.window.ImeOnBackInvokedDispatcher; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.compat.IPlatformCompat; +import com.android.internal.inputmethod.DirectBootAwareness; import com.android.internal.inputmethod.IInputMethod; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IInputMethodSession; @@ -269,8 +270,15 @@ public class InputMethodManagerServiceTestBase { LocalServices.removeServiceForTest(InputMethodManagerInternal.class); lifecycle.onStart(); - // Emulate that the user initialization is done. + // Certain tests rely on TEST_IME_ID that is installed with AndroidTest.xml. + // TODO(b/352615651): Consider just synthesizing test InputMethodInfo then injecting it. AdditionalSubtypeMapRepository.ensureInitializedAndGet(mCallingUserId); + final var settings = InputMethodManagerService.queryInputMethodServicesInternal(mContext, + mCallingUserId, AdditionalSubtypeMapRepository.get(mCallingUserId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(mCallingUserId, settings); + + // Emulate that the user initialization is done. mInputMethodManagerService.getUserData(mCallingUserId).mBackgroundLoadLatch.countDown(); // After this boot phase, services can broadcast Intents. @@ -283,6 +291,8 @@ public class InputMethodManagerServiceTestBase { @After public void tearDown() { + InputMethodSettingsRepository.remove(mCallingUserId); + if (mInputMethodManagerService != null) { mInputMethodManagerService.mInputMethodDeviceConfigs.destroy(); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 2857619c70d3..3cf895ee7204 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -40,7 +40,7 @@ import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; import androidx.annotation.NonNull; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.inputmethod.StartInputFlags; diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java index 05c243fda2b8..36baacc529d5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java @@ -16,6 +16,8 @@ package com.android.server.display; +import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -301,7 +303,7 @@ public class BrightnessThrottlerTest { new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f); List<ThrottlingLevel> levels = new ArrayList<>(List.of(level)); final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels); - final SensorData tempSensor = new SensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); + final SensorData tempSensor = createSensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); final BrightnessThrottler throttler = createThrottlerSupportedWithTempSensor(data, tempSensor); assertTrue(throttler.deviceSupportsThrottling()); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index d2686372f550..2b03dc4f78ad 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -31,6 +31,7 @@ import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.server.display.ExternalDisplayPolicy.ENABLE_ON_CONNECT; import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX; +import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -2423,7 +2424,7 @@ public class DisplayManagerServiceTest { String testSensorType = "testType"; Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName); - SensorData sensorData = new SensorData(testSensorType, testSensorName, + SensorData sensorData = createSensorData(testSensorType, testSensorName, /* minRefreshRate= */ 10f, /* maxRefreshRate= */ 100f); when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(sensorData); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index c6aea5a290e9..8ed38a6d0cad 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -21,6 +21,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE; +import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -85,7 +86,6 @@ import com.android.server.display.brightness.clamper.HdrClamper; import com.android.server.display.color.ColorDisplayService; import com.android.server.display.config.HighBrightnessModeData; import com.android.server.display.config.HysteresisLevels; -import com.android.server.display.config.SensorData; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.feature.flags.Flags; import com.android.server.display.layout.Layout; @@ -2159,13 +2159,13 @@ public final class DisplayPowerControllerTest { when(displayDeviceMock.getNameLocked()).thenReturn(displayName); when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock); when(displayDeviceConfigMock.getProximitySensor()).thenReturn( - new SensorData(Sensor.STRING_TYPE_PROXIMITY, null)); + createSensorData(Sensor.STRING_TYPE_PROXIMITY)); when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500}); when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true); when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn( - new SensorData()); + createSensorData()); when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn( - new SensorData(Sensor.STRING_TYPE_LIGHT, null)); + createSensorData(Sensor.STRING_TYPE_LIGHT)); when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux()) .thenReturn(new int[0]); when(displayDeviceConfigMock.getDefaultDozeBrightness()) diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java index ebd6614aba14..29f07227a12d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java @@ -17,6 +17,7 @@ package com.android.server.display; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -37,7 +38,6 @@ import android.view.Display; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.server.display.config.SensorData; import com.android.server.testutils.OffsettableClock; import org.junit.Before; @@ -75,7 +75,7 @@ public final class DisplayPowerProximityStateControllerTest { mClock = new OffsettableClock.Stopped(); mTestLooper = new TestLooper(mClock::now); when(mDisplayDeviceConfig.getProximitySensor()).thenReturn( - new SensorData(Sensor.STRING_TYPE_PROXIMITY, null)); + createSensorData(Sensor.STRING_TYPE_PROXIMITY)); setUpProxSensor(); DisplayPowerProximityStateController.Injector injector = new DisplayPowerProximityStateController.Injector() { @@ -165,7 +165,7 @@ public final class DisplayPowerProximityStateControllerTest { @Test public void isProximitySensorAvailableReturnsFalseWhenNotAvailableAndNoDefault() { - when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData()); + when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(createSensorData()); mDisplayPowerProximityStateController = new DisplayPowerProximityStateController( mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(), mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY, @@ -176,7 +176,7 @@ public final class DisplayPowerProximityStateControllerTest { @Test public void isProximitySensorAvailableReturnsTrueWhenNotAvailableAndHasDefault() throws Exception { - when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData()); + when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(createSensorData()); when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn( TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity")); mDisplayPowerProximityStateController = new DisplayPowerProximityStateController( @@ -189,7 +189,7 @@ public final class DisplayPowerProximityStateControllerTest { @Test public void isProximitySensorAvailableReturnsFalseWhenNotAvailableHasDefaultNonDefaultDisplay() throws Exception { - when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData()); + when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(createSensorData()); when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn( TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity")); mDisplayPowerProximityStateController = new DisplayPowerProximityStateController( @@ -216,7 +216,7 @@ public final class DisplayPowerProximityStateControllerTest { public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception { DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class); when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn( - new SensorData(Sensor.STRING_TYPE_PROXIMITY, null)); + createSensorData(Sensor.STRING_TYPE_PROXIMITY)); Sensor newProxSensor = TestUtils.createSensor( Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 4.0f); when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL))) diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index e982153acbd1..0ce9233b4b6f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,8 +42,8 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.server.display.DisplayBrightnessState; -import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState; import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.feature.DisplayManagerFlags; @@ -89,6 +90,10 @@ public class BrightnessClamperControllerTest { @Mock private BrightnessModifier mMockModifier; @Mock + private TestStatefulModifier mMockStatefulModifier; + @Mock + private TestDisplayListenerModifier mMockDisplayListenerModifier; + @Mock private DisplayManagerInternal.DisplayPowerRequest mMockRequest; @Mock @@ -99,7 +104,8 @@ public class BrightnessClamperControllerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mTestInjector = new TestInjector(List.of(mMockClamper), List.of(mMockModifier)); + mTestInjector = new TestInjector(List.of(mMockClamper), + List.of(mMockModifier, mMockStatefulModifier, mMockDisplayListenerModifier)); when(mMockDisplayDeviceData.getDisplayId()).thenReturn(DISPLAY_ID); when(mMockDisplayDeviceData.getAmbientLightSensor()).thenReturn(mMockSensorData); @@ -168,6 +174,13 @@ public class BrightnessClamperControllerTest { } @Test + public void testOnDisplayChanged_DelegatesToDisplayListeners() { + mClamperController.onDisplayChanged(mMockDisplayDeviceData); + + verify(mMockDisplayListenerModifier).onDisplayChanged(mMockDisplayDeviceData); + } + + @Test public void testOnDisplayChanged_doesNotRestartLightSensor() { mClamperController.onDisplayChanged(mMockDisplayDeviceData); @@ -189,6 +202,8 @@ public class BrightnessClamperControllerTest { mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON); verify(mMockModifier).apply(eq(mMockRequest), any()); + verify(mMockDisplayListenerModifier).apply(eq(mMockRequest), any()); + verify(mMockStatefulModifier).apply(eq(mMockRequest), any()); } @Test @@ -326,11 +341,41 @@ public class BrightnessClamperControllerTest { verify(mMockClamper).stop(); } + @Test + public void test_doesNotNotifyExternalListener_aggregatedStateNotChanged() { + mTestInjector.mCapturedChangeListener.onChanged(); + mTestHandler.flush(); + + verify(mMockExternalListener, never()).onChanged(); + } + + @Test + public void test_notifiesExternalListener_aggregatedStateChanged() { + doAnswer((invocation) -> { + ModifiersAggregatedState argument = invocation.getArgument(0); + // we need to do changes in AggregatedState to trigger onChange + argument.mMaxHdrBrightness = 0.5f; + return null; + }).when(mMockStatefulModifier).applyStateChange(any()); + mTestInjector.mCapturedChangeListener.onChanged(); + mTestHandler.flush(); + + verify(mMockExternalListener).onChanged(); + } + private BrightnessClamperController createBrightnessClamperController() { return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener, mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager); } + interface TestDisplayListenerModifier extends BrightnessStateModifier, + BrightnessClamperController.DisplayDeviceDataListener { + } + + interface TestStatefulModifier extends BrightnessStateModifier, + BrightnessClamperController.StatefulModifier { + } + private class TestInjector extends BrightnessClamperController.Injector { private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>> @@ -366,7 +411,7 @@ public class BrightnessClamperControllerTest { @Override List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, Handler handler, BrightnessClamperController.ClamperChangeListener listener, - DisplayDeviceConfig displayDeviceConfig) { + BrightnessClamperController.DisplayDeviceData displayDeviceData) { return mModifiers; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java index 34f352e7bf54..9d16594fae93 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java @@ -16,6 +16,8 @@ package com.android.server.display.brightness.clamper; +import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -189,7 +191,7 @@ public class BrightnessThermalClamperTest { final int severity = PowerManager.THERMAL_STATUS_SEVERE; IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); // Update config to listen to display type sensor. - final SensorData tempSensor = new SensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); + final SensorData tempSensor = createSensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); final TestThermalData thermalData = new TestThermalData( DISPLAY_ID, diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt new file mode 100644 index 000000000000..5fd848f6adcc --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt @@ -0,0 +1,44 @@ +/* + * 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.server.display.brightness.clamper + +import android.os.IBinder +import android.view.Display +import com.android.server.display.DisplayDeviceConfig +import com.android.server.display.brightness.clamper.BrightnessClamperController.DisplayDeviceData + +fun createDisplayDeviceData( + displayDeviceConfig: DisplayDeviceConfig, + displayToken: IBinder, + uniqueDisplayId: String = "displayId", + thermalThrottlingDataId: String = "thermalId", + powerThrottlingDataId: String = "powerId", + width: Int = 100, + height: Int = 100, + displayId: Int = Display.DEFAULT_DISPLAY +): DisplayDeviceData { + return DisplayDeviceData( + uniqueDisplayId, + thermalThrottlingDataId, + powerThrottlingDataId, + displayDeviceConfig, + width, + height, + displayToken, + displayId + ) +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt new file mode 100644 index 000000000000..0ed96ae52491 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt @@ -0,0 +1,405 @@ +/* + * 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.server.display.brightness.clamper + +import android.hardware.display.DisplayManagerInternal +import android.os.IBinder +import android.os.PowerManager.BRIGHTNESS_MAX +import android.util.Spline +import android.view.SurfaceControlHdrLayerInfoListener +import androidx.test.filters.SmallTest +import com.android.server.display.DisplayBrightnessState +import com.android.server.display.DisplayBrightnessState.BRIGHTNESS_NOT_SET +import com.android.server.display.DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET +import com.android.server.display.DisplayDeviceConfig +import com.android.server.display.brightness.clamper.BrightnessClamperController.ClamperChangeListener +import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState +import com.android.server.display.brightness.clamper.HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO +import com.android.server.display.brightness.clamper.HdrBrightnessModifier.Injector +import com.android.server.display.config.HdrBrightnessData +import com.android.server.display.config.createHdrBrightnessData +import com.android.server.testutils.OffsettableClock +import com.android.server.testutils.TestHandler +import com.google.common.truth.Truth.assertThat + +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +private const val SEND_TIME_TOLERANCE: Long = 100 + +@SmallTest +class HdrBrightnessModifierTest { + + private val stoppedClock = OffsettableClock.Stopped() + private val testHandler = TestHandler(null, stoppedClock) + private val testInjector = TestInjector() + private val mockChangeListener = mock<ClamperChangeListener>() + private val mockDisplayDeviceConfig = mock<DisplayDeviceConfig>() + private val mockDisplayBinder = mock<IBinder>() + private val mockDisplayBinderOther = mock<IBinder>() + private val mockSpline = mock<Spline>() + private val mockRequest = mock<DisplayManagerInternal.DisplayPowerRequest>() + + private lateinit var modifier: HdrBrightnessModifier + private val dummyData = createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinder) + + @Test + fun `change listener is not called on init`() { + initHdrModifier() + + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `hdr listener registered on init if hdr data is present`() { + initHdrModifier() + + assertThat(testInjector.registeredHdrListener).isNotNull() + assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinder) + } + + @Test + fun `hdr listener not registered on init if hdr data is missing`() { + initHdrModifier(null) + + testHandler.flush() + + assertThat(testInjector.registeredHdrListener).isNull() + assertThat(testInjector.registeredToken).isNull() + } + + @Test + fun `unsubscribes hdr listener when display changed with no hdr data`() { + initHdrModifier() + + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null) + modifier.onDisplayChanged(dummyData) + testHandler.flush() + + assertThat(testInjector.registeredHdrListener).isNull() + assertThat(testInjector.registeredToken).isNull() + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `resubscribes hdr listener when display changed with different token`() { + initHdrModifier() + + modifier.onDisplayChanged( + createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinderOther)) + testHandler.flush() + + assertThat(testInjector.registeredHdrListener).isNotNull() + assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinderOther) + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `test NO_HDR mode`() { + initHdrModifier() + // screen size = 10_000 + setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + sdrToHdrRatioSpline = mockSpline + )) + + // hdr size = 900 + val desiredMaxHdrRatio = 8f + setupHdrLayer(width = 30, height = 30, maxHdrRatio = desiredMaxHdrRatio) + + assertModifierState() + } + + @Test + fun `test NBM_HDR mode`() { + initHdrModifier() + // screen size = 10_000 + val transitionPoint = 0.55f + setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + transitionPoint = transitionPoint, + sdrToHdrRatioSpline = mockSpline + )) + // hdr size = 5_100 + val desiredMaxHdrRatio = 8f + setupHdrLayer(width = 100, height = 51, maxHdrRatio = desiredMaxHdrRatio) + + whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr( + 0f, desiredMaxHdrRatio, mockSpline)).thenReturn(0.85f) + + assertModifierState( + maxBrightness = transitionPoint, + hdrRatio = desiredMaxHdrRatio, + hdrBrightness = transitionPoint, + spline = mockSpline + ) + } + + @Test + fun `test HBM_HDR mode`() { + initHdrModifier() + // screen size = 10_000 + setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + transitionPoint = 0.55f, + sdrToHdrRatioSpline = mockSpline + )) + // hdr size = 7_100 + val desiredMaxHdrRatio = 8f + setupHdrLayer(width = 100, height = 71, maxHdrRatio = desiredMaxHdrRatio) + + val expectedHdrBrightness = 0.92f + whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr( + 0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness) + + assertModifierState( + hdrRatio = desiredMaxHdrRatio, + hdrBrightness = expectedHdrBrightness, + spline = mockSpline + ) + } + + @Test + fun `test display change no HDR content`() { + initHdrModifier() + setupDisplay(width = 100, height = 100) + assertModifierState() + clearInvocations(mockChangeListener) + // display change, new instance of HdrBrightnessData + setupDisplay(width = 100, height = 100) + + assertModifierState() + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `test display change with HDR content`() { + initHdrModifier() + setupDisplay(width = 100, height = 100) + setupHdrLayer(width = 100, height = 100, maxHdrRatio = 5f) + assertModifierState( + hdrBrightness = 0f, + hdrRatio = 5f, + spline = mockSpline + ) + clearInvocations(mockChangeListener) + // display change, new instance of HdrBrightnessData + setupDisplay(width = 100, height = 100) + + assertModifierState( + hdrBrightness = 0f, + hdrRatio = 5f, + spline = mockSpline + ) + // new instance of HdrBrightnessData received, notify listener + verify(mockChangeListener).onChanged() + } + + @Test + fun `test ambient lux decrease above maxBrightnessLimits no HDR`() { + initHdrModifier() + modifier.setAmbientLux(1000f) + setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData( + maxBrightnessLimits = mapOf(Pair(500f, 0.6f)) + )) + + modifier.setAmbientLux(500f) + // verify debounce is not scheduled + assertThat(testHandler.hasMessagesOrCallbacks()).isFalse() + + assertModifierState() + verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any()) + } + + @Test + fun `test ambient lux decrease above maxBrightnessLimits with HDR`() { + initHdrModifier() + modifier.setAmbientLux(1000f) + setupDisplay(width = 200, height = 200, hdrBrightnessData = createHdrBrightnessData( + maxBrightnessLimits = mapOf(Pair(500f, 0.6f)), + sdrToHdrRatioSpline = mockSpline + )) + setupHdrLayer(width = 200, height = 200, maxHdrRatio = 8f) + + modifier.setAmbientLux(500f) + + // verify debounce is not scheduled + assertThat(testHandler.hasMessagesOrCallbacks()).isFalse() + + val hdrBrightnessFromSdr = 0.83f + whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr( + 0f, 8f, mockSpline)).thenReturn(hdrBrightnessFromSdr) + + assertModifierState( + hdrBrightness = hdrBrightnessFromSdr, + spline = mockSpline, + hdrRatio = 8f + ) + } + + @Test + fun `test ambient lux decrease below maxBrightnessLimits no HDR`() { + initHdrModifier() + modifier.setAmbientLux(1000f) + setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData( + maxBrightnessLimits = mapOf(Pair(500f, 0.6f)) + )) + + modifier.setAmbientLux(499f) + // verify debounce is not scheduled + assertThat(testHandler.hasMessagesOrCallbacks()).isFalse() + + assertModifierState() + verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any()) + } + + @Test + fun `test ambient lux decrease below maxBrightnessLimits with HDR`() { + initHdrModifier() + modifier.setAmbientLux(1000f) + val maxBrightness = 0.6f + val brightnessDecreaseDebounceMillis = 2800L + val animationRate = 0.01f + setupDisplay(width = 200, height = 200, hdrBrightnessData = createHdrBrightnessData( + maxBrightnessLimits = mapOf(Pair(500f, maxBrightness)), + brightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis, + screenBrightnessRampDecrease = animationRate, + sdrToHdrRatioSpline = mockSpline, + )) + setupHdrLayer(width = 200, height = 200, maxHdrRatio = 8f) + + modifier.setAmbientLux(499f) + + val hdrBrightnessFromSdr = 0.83f + whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr( + 0f, 8f, mockSpline)).thenReturn(hdrBrightnessFromSdr) + // debounce with brightnessDecreaseDebounceMillis, no changes to the state just yet + assertModifierState( + hdrBrightness = hdrBrightnessFromSdr, + spline = mockSpline, + hdrRatio = 8f + ) + + // verify debounce is scheduled + assertThat(testHandler.hasMessagesOrCallbacks()).isTrue() + val msgInfo = testHandler.pendingMessages.peek() + assertSendTime(brightnessDecreaseDebounceMillis, msgInfo!!.sendTime) + clearInvocations(mockChangeListener) + + // triggering debounce, state changes + testHandler.flush() + + verify(mockChangeListener).onChanged() + + assertModifierState( + hdrBrightness = maxBrightness, + spline = mockSpline, + hdrRatio = 8f, + maxBrightness = maxBrightness, + animationRate = animationRate + ) + } + + private fun setupHdrLayer(width: Int = 100, height: Int = 100, maxHdrRatio: Float = 0.8f) { + testInjector.registeredHdrListener!!.onHdrInfoChanged( + mockDisplayBinder, 1, width, height, 0, maxHdrRatio + ) + testHandler.flush() + } + + private fun setupDisplay( + width: Int = 100, + height: Int = 100, + hdrBrightnessData: HdrBrightnessData? = createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + transitionPoint = 0.68f, + sdrToHdrRatioSpline = mockSpline + ) + ) { + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(hdrBrightnessData) + modifier.onDisplayChanged(createDisplayDeviceData( + mockDisplayDeviceConfig, mockDisplayBinder, + width = width, + height = height + )) + testHandler.flush() + } + + private fun initHdrModifier(hdrBrightnessData: HdrBrightnessData? = createHdrBrightnessData()) { + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(hdrBrightnessData) + modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData) + testHandler.flush() + } + + // MsgInfo.sendTime is calculated first by adding SystemClock.uptimeMillis() + // (in Handler.sendMessageDelayed) and then by subtracting SystemClock.uptimeMillis() + // (in TestHandler.sendMessageAtTime, there might be several milliseconds difference between + // SystemClock.uptimeMillis() calls, and subtracted value might be greater than added. + private fun assertSendTime(expectedTime: Long, sendTime: Long) { + assertThat(sendTime).isAtMost(expectedTime) + assertThat(sendTime).isGreaterThan(expectedTime - SEND_TIME_TOLERANCE) + } + + private fun assertModifierState( + maxBrightness: Float = BRIGHTNESS_MAX, + hdrRatio: Float = DEFAULT_MAX_HDR_SDR_RATIO, + spline: Spline? = null, + hdrBrightness: Float = BRIGHTNESS_NOT_SET, + animationRate: Float = CUSTOM_ANIMATION_RATE_NOT_SET + ) { + val modifierState = ModifiersAggregatedState() + modifier.applyStateChange(modifierState) + + assertThat(modifierState.mMaxHdrBrightness).isEqualTo(maxBrightness) + assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(hdrRatio) + assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(spline) + + val stateBuilder = DisplayBrightnessState.builder() + modifier.apply(mockRequest, stateBuilder) + + assertThat(stateBuilder.hdrBrightness).isEqualTo(hdrBrightness) + assertThat(stateBuilder.customAnimationRate).isEqualTo(animationRate) + } + + internal class TestInjector : Injector() { + var registeredHdrListener: SurfaceControlHdrLayerInfoListener? = null + var registeredToken: IBinder? = null + + override fun registerHdrListener( + listener: SurfaceControlHdrLayerInfoListener, token: IBinder + ) { + registeredHdrListener = listener + registeredToken = token + } + + override fun unregisterHdrListener( + listener: SurfaceControlHdrLayerInfoListener, token: IBinder + ) { + registeredHdrListener = null + registeredToken = null + } + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt index b742d021f2e9..f59e1275d2ce 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt @@ -26,6 +26,7 @@ import com.android.server.display.TestUtils import com.android.server.display.brightness.clamper.LightSensorController.Injector import com.android.server.display.brightness.clamper.LightSensorController.LightSensorListener import com.android.server.display.config.SensorData +import com.android.server.display.config.createSensorData import com.android.server.display.utils.AmbientFilter import org.junit.Before import org.mockito.kotlin.any @@ -51,7 +52,7 @@ class LightSensorControllerTest { private val mockAmbientFilter: AmbientFilter = mock() private val testInjector = TestInjector() - private val dummySensorData = SensorData() + private val dummySensorData = createSensorData() private lateinit var controller: LightSensorController diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt index 3b3d6f74da50..c7580331c841 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt @@ -24,6 +24,17 @@ import java.io.ByteArrayOutputStream import java.io.OutputStreamWriter import org.xmlpull.v1.XmlSerializer +@JvmOverloads +fun createSensorData( + type: String? = null, + name: String? = null, + minRefreshRate: Float = 0f, + maxRefreshRate: Float = Float.POSITIVE_INFINITY, + supportedModes: List<SupportedModeData> = emptyList() +): SensorData { + return SensorData(type, name, minRefreshRate, maxRefreshRate, supportedModes) +} + fun createRefreshRateData( defaultRefreshRate: Int = 60, defaultPeakRefreshRate: Int = 60, @@ -46,6 +57,7 @@ fun createHdrBrightnessData( screenBrightnessRampIncrease: Float = 0.02f, brightnessDecreaseDebounceMillis: Long = 3000, screenBrightnessRampDecrease: Float = 0.04f, + transitionPoint: Float = 0.65f, minimumHdrPercentOfScreenForNbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT, minimumHdrPercentOfScreenForHbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT, allowInLowPowerMode: Boolean = false, @@ -57,6 +69,7 @@ fun createHdrBrightnessData( screenBrightnessRampIncrease, brightnessDecreaseDebounceMillis, screenBrightnessRampDecrease, + transitionPoint, minimumHdrPercentOfScreenForNbm, minimumHdrPercentOfScreenForHbm, allowInLowPowerMode, diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt index 19c6924dfa5b..917c681a0d95 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt @@ -16,6 +16,7 @@ package com.android.server.display.config +import android.os.PowerManager import android.util.Spline.createSpline import androidx.test.filters.SmallTest import com.android.server.display.DisplayBrightnessState @@ -42,7 +43,7 @@ class HdrBrightnessDataTest { ) } - val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) { 0.6f } assertThat(hdrBrightnessData).isNotNull() assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(3000) @@ -54,6 +55,7 @@ class HdrBrightnessDataTest { assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(500f, 0.6f) assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(600f, 0.7f) + assertThat(hdrBrightnessData.hbmTransitionPoint).isEqualTo(PowerManager.BRIGHTNESS_MAX) assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo( HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT ) @@ -79,10 +81,13 @@ class HdrBrightnessDataTest { ) } - val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + val transitionPoint = 0.6f + val hdrBrightnessData = + HdrBrightnessData.loadConfig(displayConfiguration) { transitionPoint } assertThat(hdrBrightnessData).isNotNull() - assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f) + assertThat(hdrBrightnessData!!.hbmTransitionPoint).isEqualTo(transitionPoint) + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f) assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f) assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse() @@ -100,7 +105,9 @@ class HdrBrightnessDataTest { ) } - val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + val transitionPoint = 0.6f + val hdrBrightnessData = + HdrBrightnessData.loadConfig(displayConfiguration) { transitionPoint } assertThat(hdrBrightnessData).isNotNull() assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(0) @@ -112,6 +119,7 @@ class HdrBrightnessDataTest { assertThat(hdrBrightnessData.maxBrightnessLimits).hasSize(0) + assertThat(hdrBrightnessData.hbmTransitionPoint).isEqualTo(transitionPoint) assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f) assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f) assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse() @@ -125,7 +133,7 @@ class HdrBrightnessDataTest { fun `test HdrBrightnessData configuration no configuration`() { val displayConfiguration = createDisplayConfiguration() - val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) { 0.6f } assertThat(hdrBrightnessData).isNull() } @@ -144,10 +152,13 @@ class HdrBrightnessDataTest { ) } - val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) + val transitionPoint = 0.6f + val hdrBrightnessData = + HdrBrightnessData.loadConfig(displayConfiguration) { transitionPoint } assertThat(hdrBrightnessData).isNotNull() - assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.3f) + assertThat(hdrBrightnessData!!.hbmTransitionPoint).isEqualTo(transitionPoint) + assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.3f) assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.6f) assertThat(hdrBrightnessData.allowInLowPowerMode).isTrue() diff --git a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java index 6e2d954c05a8..c0f5e7a69af7 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java @@ -16,6 +16,8 @@ package com.android.server.display.utils; +import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.when; @@ -65,7 +67,7 @@ public class SensorUtilsTest { @Test public void testNoSensorManager() { - Sensor result = SensorUtils.findSensor(null, new SensorData(), Sensor.TYPE_LIGHT); + Sensor result = SensorUtils.findSensor(null, createSensorData(), Sensor.TYPE_LIGHT); assertNull(result); } @@ -123,7 +125,7 @@ public class SensorUtilsTest { when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(allSensors); when(mSensorManager.getDefaultSensor(fallbackType)).thenReturn(defaultSensor); - SensorData sensorData = new SensorData(sensorType, sensorName); + SensorData sensorData = createSensorData(sensorType, sensorName); Sensor result = SensorUtils.findSensor(mSensorManager, sensorData, fallbackType); diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java index 99968d5117c7..9da695a4effb 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java @@ -19,10 +19,12 @@ package com.android.server.dreams; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + import android.content.Context; import android.content.res.Resources; @@ -44,9 +46,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; -import java.util.Collections; - @SmallTest @RunWith(AndroidJUnit4.class) public class DreamAccessibilityTest { @@ -73,7 +72,8 @@ public class DreamAccessibilityTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mDreamAccessibility = new DreamAccessibility(mContext, mView); + Runnable mDismissCallback = () -> {}; + mDreamAccessibility = new DreamAccessibility(mContext, mView, mDismissCallback); when(mContext.getResources()).thenReturn(mResources); when(mResources.getString(R.string.dream_accessibility_action_click)) @@ -84,80 +84,55 @@ public class DreamAccessibilityTest { */ @Test public void testConfigureAccessibilityActions() { - when(mAccessibilityNodeInfo.getActionList()).thenReturn(new ArrayList<>()); + when(mView.getAccessibilityDelegate()).thenReturn(null); - mDreamAccessibility.updateAccessibilityConfiguration(false); + mDreamAccessibility.updateAccessibilityConfiguration(); verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture()); - View.AccessibilityDelegate capturedDelegate = - mAccessibilityDelegateArgumentCaptor.getValue(); + View.AccessibilityDelegate capturedDelegate = mAccessibilityDelegateArgumentCaptor + .getValue(); capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo); verify(mAccessibilityNodeInfo).addAction(argThat(action -> - action.getId() == AccessibilityNodeInfo.ACTION_CLICK + action.getId() == AccessibilityNodeInfo.ACTION_DISMISS && TextUtils.equals(action.getLabel(), CUSTOM_ACTION))); } /** - * Test to verify the configuration of accessibility actions within a view delegate, - * specifically checking the removal of an existing click action and addition - * of a new custom action. + * Test to verify no accessibility configuration is added if one exist. */ @Test - public void testConfigureAccessibilityActions_RemovesExistingClickAction() { - AccessibilityNodeInfo.AccessibilityAction existingAction = - new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, - EXISTING_ACTION); - when(mAccessibilityNodeInfo.getActionList()) - .thenReturn(Collections.singletonList(existingAction)); - - mDreamAccessibility.updateAccessibilityConfiguration(false); - - verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture()); - View.AccessibilityDelegate capturedDelegate = - mAccessibilityDelegateArgumentCaptor.getValue(); - - capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo); + public void testNotAddingDuplicateAccessibilityConfiguration() { + View.AccessibilityDelegate existingDelegate = mock(View.AccessibilityDelegate.class); + when(mView.getAccessibilityDelegate()).thenReturn(existingDelegate); - verify(mAccessibilityNodeInfo).removeAction(existingAction); - verify(mAccessibilityNodeInfo).addAction(argThat(action -> - action.getId() == AccessibilityNodeInfo.ACTION_CLICK - && TextUtils.equals(action.getLabel(), CUSTOM_ACTION))); + mDreamAccessibility.updateAccessibilityConfiguration(); + verify(mView, never()).setAccessibilityDelegate(any()); } /** - * Test to verify the removal of a custom accessibility action within a view delegate. + * Test to verify dismiss callback is called */ @Test - public void testRemoveCustomAccessibilityAction() { + public void testPerformAccessibilityAction() { + Runnable mockDismissCallback = mock(Runnable.class); + DreamAccessibility dreamAccessibility = new DreamAccessibility(mContext, + mView, mockDismissCallback); - AccessibilityNodeInfo.AccessibilityAction existingAction = - new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, - EXISTING_ACTION); - when(mAccessibilityNodeInfo.getActionList()) - .thenReturn(Collections.singletonList(existingAction)); + dreamAccessibility.updateAccessibilityConfiguration(); - mDreamAccessibility.updateAccessibilityConfiguration(false); verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture()); - View.AccessibilityDelegate capturedDelegate = - mAccessibilityDelegateArgumentCaptor.getValue(); - when(mView.getAccessibilityDelegate()).thenReturn(capturedDelegate); - clearInvocations(mView); + View.AccessibilityDelegate capturedDelegate = mAccessibilityDelegateArgumentCaptor + .getValue(); - mDreamAccessibility.updateAccessibilityConfiguration(true); - verify(mView).setAccessibilityDelegate(null); - } + boolean result = capturedDelegate.performAccessibilityAction(mView, + AccessibilityNodeInfo.ACTION_DISMISS, null); - /** - * Test to verify the removal of custom accessibility action is not called if delegate is not - * set by the dreamService. - */ - @Test - public void testRemoveCustomAccessibility_DoesNotRemoveDelegateNotSetByDreamAccessibility() { - mDreamAccessibility.updateAccessibilityConfiguration(true); - verify(mView, never()).setAccessibilityDelegate(any()); + assertTrue(result); + verify(mockDismissCallback).run(); } + } diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java index 72883e269a65..5bd919f28e6a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java @@ -23,6 +23,7 @@ import static com.android.server.alarm.UserWakeupStore.USER_START_TIME_DEVIATION import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.testng.AssertJUnit.assertFalse; import android.os.Environment; import android.os.FileUtils; @@ -51,6 +52,7 @@ public class UserWakeupStoreTest { private static final int USER_ID_1 = 10; private static final int USER_ID_2 = 11; private static final int USER_ID_3 = 12; + private static final int USER_ID_SYSTEM = 0; private static final long TEST_TIMESTAMP = 150_000; private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry .getInstrumentation().getContext().getDataDir(), "alarmsTestDir"); @@ -110,6 +112,14 @@ public class UserWakeupStoreTest { } @Test + public void testAddWakeupForSystemUser_shouldDoNothing() { + mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000); + assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length); + final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml"); + assertFalse(file.exists()); + } + + @Test public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() { final long finalAlarmTime = TEST_TIMESTAMP - 13_000; mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 1dbd5320cac6..8656b991b5fc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -500,6 +500,13 @@ public class MockingOomAdjusterTests { updateOomAdj(app); assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); assertEquals("resumed-split-screen-activity", app.mState.getAdjType()); + + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE + | WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) + .when(wpc).getActivityStateFlags(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); + assertEquals("perceptible-freeform-activity", app.mState.getAdjType()); } @SuppressWarnings("GuardedBy") diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index f2b4136c51ed..b2a5b02c49e1 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -59,6 +59,7 @@ android_ravenwood_test { name: "PowerStatsTestsRavenwood", static_libs: [ "services.core", + "platformprotosnano", "coretests-aidl", "ravenwood-junit", "truth", diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java index ac1f7d0e345f..37d8f2f74850 100644 --- a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.internal.os; +package com.android.server.power.stats; import static android.os.BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE; @@ -23,39 +23,262 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import android.os.AggregateBatteryConsumer; import android.os.BatteryConsumer; import android.os.BatteryUsageStats; +import android.os.Process; import android.os.UidBatteryConsumer; import android.os.nano.BatteryUsageStatsAtomsProto; import android.os.nano.BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.StatsEvent; import androidx.test.filters.SmallTest; +import com.android.server.am.BatteryStatsService; + import com.google.protobuf.nano.InvalidProtocolBufferNanoException; +import org.junit.Rule; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; - @SmallTest -public class BatteryUsageStatsPulledTest { +public class BatteryUsageStatsAtomTest { + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); private static final int UID_0 = 1000; private static final int UID_1 = 2000; private static final int UID_2 = 3000; private static final int UID_3 = 4000; - private static final int[] UID_USAGE_TIME_PROCESS_STATES = { - BatteryConsumer.PROCESS_STATE_FOREGROUND, - BatteryConsumer.PROCESS_STATE_BACKGROUND, - BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE - }; @Test - public void testGetStatsProto() { + public void testAtom_BatteryUsageStatsPerUid() { + final BatteryUsageStats bus = buildBatteryUsageStats(); + BatteryStatsService.FrameworkStatsLogger statsLogger = + mock(BatteryStatsService.FrameworkStatsLogger.class); + + List<StatsEvent> actual = new ArrayList<>(); + new BatteryStatsService.StatsPerUidLogger(statsLogger).logStats(bus, actual); + + // Device-wide totals + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + Process.INVALID_UID, + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + 0L, + "cpu", + 30000.0f, + 20100.0f, + 20300L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + Process.INVALID_UID, + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + 0L, + "camera", + 30000.0f, + 20150.0f, + 0L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + Process.INVALID_UID, + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + 0L, + "CustomConsumer1", + 30000.0f, + 20200.0f, + 20400L + ); + + // Per-proc state estimates for UID_0 + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + 0L, + "screen", + 1650.0f, + 300.0f, + 0L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + 0L, + "cpu", + 1650.0f, + 400.0f, + 600L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_FOREGROUND, + 1000L, + "cpu", + 1650.0f, + 9100.0f, + 8100L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_BACKGROUND, + 2000L, + "cpu", + 1650.0f, + 9200.0f, + 8200L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE, + 0L, + "cpu", + 1650.0f, + 9300.0f, + 8400L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_CACHED, + 0L, + "cpu", + 1650.0f, + 9400.0f, + 0L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_FOREGROUND, + 1000L, + "CustomConsumer1", + 1650.0f, + 450.0f, + 0L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_BACKGROUND, + 2000L, + "CustomConsumer1", + 1650.0f, + 450.0f, + 0L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_FOREGROUND, + 1000L, + "CustomConsumer2", + 1650.0f, + 500.0f, + 800L + ); + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_0, + BatteryConsumer.PROCESS_STATE_BACKGROUND, + 2000L, + "CustomConsumer2", + 1650.0f, + 500.0f, + 800L + ); + + // Nothing for UID_1, because its power consumption is 0 + + // Only "screen" is populated for UID_2 + verify(statsLogger).buildStatsEvent( + 1000L, + 20000L, + 10000L, + 20, + 1234L, + UID_2, + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + 0L, + "screen", + 766.0f, + 766.0f, + 0L + ); + + verifyNoMoreInteractions(statsLogger); + } + + @Test + public void testAtom_BatteryUsageStatsAtomsProto() { final BatteryUsageStats bus = buildBatteryUsageStats(); final byte[] bytes = bus.getStatsProto(); BatteryUsageStatsAtomsProto proto; @@ -68,9 +291,7 @@ public class BatteryUsageStatsPulledTest { assertEquals(bus.getStatsStartTimestamp(), proto.sessionStartMillis); assertEquals(bus.getStatsEndTimestamp(), proto.sessionEndMillis); - assertEquals( - bus.getStatsEndTimestamp() - bus.getStatsStartTimestamp(), - proto.sessionDurationMillis); + assertEquals(10000, proto.sessionDurationMillis); assertEquals(bus.getDischargePercentage(), proto.sessionDischargePercentage); assertEquals(bus.getDischargeDurationMs(), proto.dischargeDurationMillis); @@ -90,8 +311,8 @@ public class BatteryUsageStatsPulledTest { final List<android.os.UidBatteryConsumer> uidConsumers = bus.getUidBatteryConsumers(); uidConsumers.sort((a, b) -> a.getUid() - b.getUid()); - final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto - = proto.uidBatteryConsumers; + final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto = + proto.uidBatteryConsumers; Arrays.sort(uidConsumersProto, (a, b) -> a.uid - b.uid); // UID_0 - After sorting, UID_0 should be in position 0 for both data structures @@ -186,6 +407,12 @@ public class BatteryUsageStatsPulledTest { } } + private static final int[] UID_USAGE_TIME_PROCESS_STATES = { + BatteryConsumer.PROCESS_STATE_FOREGROUND, + BatteryConsumer.PROCESS_STATE_BACKGROUND, + BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE + }; + private void assertSameUidBatteryConsumer( android.os.UidBatteryConsumer uidConsumer, BatteryUsageStatsAtomsProto.UidBatteryConsumer uidConsumerProto, @@ -195,10 +422,10 @@ public class BatteryUsageStatsPulledTest { assertEquals("Uid consumers had mismatched uids", uid, uidConsumer.getUid()); assertEquals("For uid " + uid, - uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND), + uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_FOREGROUND), uidConsumerProto.timeInForegroundMillis); assertEquals("For uid " + uid, - uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND), + uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_BACKGROUND), uidConsumerProto.timeInBackgroundMillis); for (int processState : UID_USAGE_TIME_PROCESS_STATES) { final long timeInStateMillis = uidConsumer.getTimeInProcessStateMs(processState); @@ -261,11 +488,15 @@ public class BatteryUsageStatsPulledTest { new BatteryUsageStats.Builder(new String[]{"CustomConsumer1", "CustomConsumer2"}, /* includePowerModels */ true, /* includeProcessStats */ true, + /* includeScreenStateData */ false, + /* includePowerStateData */ false, /* minConsumedPowerThreshold */ 0) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) .setDischargeDurationMs(1234) - .setStatsStartTimestamp(1000); + .setStatsStartTimestamp(1000) + .setStatsEndTimestamp(20000) + .setStatsDuration(10000); final UidBatteryConsumer.Builder uidBuilder = builder .getOrCreateUidBatteryConsumerBuilder(UID_0) .setPackageWithHighestDrain("myPackage0") @@ -345,7 +576,7 @@ public class BatteryUsageStatsPulledTest { @Test public void testLargeAtomTruncated() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[0], true, false, 0); + new BatteryUsageStats.Builder(new String[0], true, false, false, false, 0); // If not truncated, this BatteryUsageStats object would generate a proto buffer // significantly larger than 50 Kb for (int i = 0; i < 3000; i++) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index 6edfedee9e5b..624b18948c49 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -397,10 +397,14 @@ public class BatteryUsageStatsRule implements TestRule { & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0; final boolean includeProcessStateData = (query.getFlags() & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0; + final boolean includeScreenStateData = (query.getFlags() + & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE) != 0; + final boolean includePowerStateData = (query.getFlags() + & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE) != 0; final double minConsumedPowerThreshold = query.getMinConsumedPowerThreshold(); BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( customPowerComponentNames, includePowerModels, includeProcessStateData, - minConsumedPowerThreshold); + includeScreenStateData, includePowerStateData, minConsumedPowerThreshold); SparseArray<? extends BatteryStats.Uid> uidStats = mBatteryStats.getUidStats(); for (int i = 0; i < uidStats.size(); i++) { builder.getOrCreateUidBatteryConsumerBuilder(uidStats.valueAt(i)); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java index a3f0770ec8ba..52bb5e839ca2 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java @@ -31,6 +31,8 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static java.util.regex.Pattern.quote; + import android.os.AggregateBatteryConsumer; import android.os.BatteryConsumer; import android.os.BatteryUsageStats; @@ -91,7 +93,7 @@ public class BatteryUsageStatsTest { final Parcel parcel = Parcel.obtain(); parcel.writeParcelable(outBatteryUsageStats, 0); - assertThat(parcel.dataSize()).isLessThan(12000); + assertThat(parcel.dataSize()).isLessThan(100000); parcel.setDataPosition(0); @@ -161,15 +163,47 @@ public class BatteryUsageStatsTest { assertThat(dump).contains("Computed drain: 30000"); assertThat(dump).contains("actual drain: 1000-2000"); assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms"); - assertThat(dump).contains("cpu(fg): 2333 apps: 1333 duration: 3s 332ms"); - assertThat(dump).contains("cpu(bg): 2444 apps: 1444 duration: 4s 442ms"); - assertThat(dump).contains("cpu(fgs): 2555 apps: 1555 duration: 5s 552ms"); - assertThat(dump).contains("cpu(cached): 123 apps: 123 duration: 456ms"); assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms"); - assertThat(dump).contains("UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123 " - + "( screen=300 cpu=400 (600ms) cpu:fg=1777 (7s 771ms) cpu:bg=1888 (8s 881ms) " - + "cpu:fgs=1999 (9s 991ms) cpu:cached=123 (456ms) FOO=500 )"); - assertThat(dump).contains("User 42: 30.0 ( cpu=10.0 (30ms) FOO=20.0 )"); + assertThat(dump).containsMatch(quote("(on battery, screen on)") + "\\s*" + + "cpu: 2333 apps: 1333 duration: 3s 332ms"); + assertThat(dump).containsMatch(quote("(not on battery, screen on)") + "\\s*" + + "cpu: 2555 apps: 1555 duration: 5s 552ms"); + assertThat(dump).containsMatch(quote("(on battery, screen off/doze)") + "\\s*" + + "cpu: 2444 apps: 1444 duration: 4s 442ms"); + assertThat(dump).containsMatch(quote("(not on battery, screen off/doze)") + "\\s*" + + "cpu: 123 apps: 123 duration: 456ms"); + assertThat(dump).containsMatch( + "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*" + + quote("screen=300 cpu=5787 (27s 99ms) cpu:fg=1777 (7s 771ms) " + + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) " + + "cpu:cached=123 (456ms) FOO=500") + "\\s*" + + quote("(on battery, screen on)") + "\\s*" + + quote("cpu:fg=1777 (7s 771ms)")); + assertThat(dump).containsMatch("User 42: 30.0\\s*" + + quote("cpu=10.0 (30ms) FOO=20.0")); + } + + @Test + public void testDumpNoScreenOrPowerState() { + final BatteryUsageStats stats = buildBatteryUsageStats1(true, false, false).build(); + final StringWriter out = new StringWriter(); + try (PrintWriter pw = new PrintWriter(out)) { + stats.dump(pw, " "); + } + final String dump = out.toString(); + + assertThat(dump).contains("Capacity: 4000"); + assertThat(dump).contains("Computed drain: 30000"); + assertThat(dump).contains("actual drain: 1000-2000"); + assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms"); + assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms"); + assertThat(dump).containsMatch( + "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*" + + quote("screen=300 cpu=5787 (600ms) cpu:fg=1777 (7s 771ms) " + + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) " + + "cpu:cached=123 (456ms) FOO=500")); + assertThat(dump).containsMatch("User 42: 30.0\\s*" + + quote("cpu=10.0 (30ms) FOO=20.0")); } @Test @@ -186,9 +220,8 @@ public class BatteryUsageStatsTest { public void testAdd() { final BatteryUsageStats stats1 = buildBatteryUsageStats1(false).build(); final BatteryUsageStats stats2 = buildBatteryUsageStats2(new String[]{"FOO"}, true).build(); - final BatteryUsageStats sum = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0) .add(stats1) .add(stats2) .build(); @@ -200,14 +233,14 @@ public class BatteryUsageStatsTest { for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) { if (uidBatteryConsumer.getUid() == APP_UID1) { assertUidBatteryConsumer(uidBatteryConsumer, 2124, null, - 5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 745, + 5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 11772, POWER_MODEL_UNDEFINED, 956, 1167, 1478, true, 3554, 3776, 3998, 444, 3554, 15542, 3776, 17762, 3998, 19982, 444, 1110); } else if (uidBatteryConsumer.getUid() == APP_UID2) { assertUidBatteryConsumer(uidBatteryConsumer, 1332, "bar", - 1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 444, + 1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5985, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 555, 666, 777, true, 1777, 1888, 1999, 321, 1777, 7771, 1888, 8881, 1999, 9991, @@ -229,7 +262,7 @@ public class BatteryUsageStatsTest { @Test public void testAdd_customComponentMismatch() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0); + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0); final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"BAR"}, false).build(); assertThrows(IllegalArgumentException.class, () -> builder.add(stats)); @@ -238,7 +271,7 @@ public class BatteryUsageStatsTest { @Test public void testAdd_processStateDataMismatch() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0); + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0); final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"FOO"}, false).build(); assertThrows(IllegalArgumentException.class, () -> builder.add(stats)); @@ -259,15 +292,23 @@ public class BatteryUsageStatsTest { parser.setInput(in, StandardCharsets.UTF_8.name()); final BatteryUsageStats fromXml = BatteryUsageStats.createFromXml(parser); + System.out.println("stats = " + stats); + System.out.println("fromXml = " + fromXml); assertBatteryUsageStats1(fromXml, true); } private BatteryUsageStats.Builder buildBatteryUsageStats1(boolean includeUserBatteryConsumer) { + return buildBatteryUsageStats1(includeUserBatteryConsumer, true, true); + } + + private BatteryUsageStats.Builder buildBatteryUsageStats1(boolean includeUserBatteryConsumer, + boolean includeScreenState, boolean includePowerState) { final MockClock clocks = new MockClock(); final MockBatteryStatsImpl batteryStats = new MockBatteryStatsImpl(clocks); final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, + includeScreenState, includePowerState, 0) .setBatteryCapacity(4000) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) @@ -312,7 +353,7 @@ public class BatteryUsageStatsTest { final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(customPowerComponentNames, true, - includeProcessStateData, 0); + includeProcessStateData, true, true, 0); builder.setDischargePercentage(30) .setDischargedPowerRange(1234, 2345) .setStatsStartTimestamp(2000) @@ -371,9 +412,15 @@ public class BatteryUsageStatsTest { .setUsageDurationForCustomComponentMillis( BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration); if (builder.isProcessStateDataNeeded()) { - final BatteryConsumer.Key cpuFgKey = uidBuilder.getKey( - BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND); + final BatteryConsumer.Key cpuFgKey = builder.isScreenStateDataNeeded() + ? uidBuilder.getKey( + BatteryConsumer.POWER_COMPONENT_CPU, + BatteryConsumer.PROCESS_STATE_FOREGROUND, + BatteryConsumer.SCREEN_STATE_ON, + BatteryConsumer.POWER_STATE_BATTERY) + : uidBuilder.getKey( + BatteryConsumer.POWER_COMPONENT_CPU, + BatteryConsumer.PROCESS_STATE_FOREGROUND); final BatteryConsumer.Key cpuBgKey = uidBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_BACKGROUND); @@ -401,9 +448,9 @@ public class BatteryUsageStatsTest { private void addAggregateBatteryConsumer(BatteryUsageStats.Builder builder, int scope, double consumedPower, int cpuPower, int customComponentPower, int cpuDuration, - int customComponentDuration, double cpuPowerForeground, long cpuDurationForeground, - double cpuPowerBackground, long cpuDurationBackground, double cpuPowerFgs, - long cpuDurationFgs, double cpuPowerCached, long cpuDurationCached) { + int customComponentDuration, double cpuPowerBatScrOn, long cpuDurationBatScrOn, + double cpuPowerBatScrOff, long cpuDurationBatScrOff, double cpuPowerChgScrOn, + long cpuDurationChgScrOn, double cpuPowerChgScrOff, long cpuDurationChgScrOff) { final AggregateBatteryConsumer.Builder aggBuilder = builder.getAggregateBatteryConsumerBuilder(scope) .setConsumedPower(consumedPower) @@ -417,32 +464,40 @@ public class BatteryUsageStatsTest { .setUsageDurationForCustomComponentMillis( BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration); - if (builder.isProcessStateDataNeeded()) { - final BatteryConsumer.Key cpuFgKey = aggBuilder.getKey( + if (builder.isPowerStateDataNeeded() || builder.isScreenStateDataNeeded()) { + final BatteryConsumer.Key cpuBatScrOn = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND); - final BatteryConsumer.Key cpuBgKey = aggBuilder.getKey( + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_ON, + BatteryConsumer.POWER_STATE_BATTERY); + final BatteryConsumer.Key cpuBatScrOff = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_BACKGROUND); - final BatteryConsumer.Key cpuFgsKey = aggBuilder.getKey( + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_OTHER, + BatteryConsumer.POWER_STATE_BATTERY); + final BatteryConsumer.Key cpuChgScrOn = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE); - final BatteryConsumer.Key cpuCachedKey = aggBuilder.getKey( + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_ON, + BatteryConsumer.POWER_STATE_OTHER); + final BatteryConsumer.Key cpuChgScrOff = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_CACHED); + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_OTHER, + BatteryConsumer.POWER_STATE_OTHER); aggBuilder - .setConsumedPower(cpuFgKey, cpuPowerForeground, + .setConsumedPower(cpuBatScrOn, cpuPowerBatScrOn, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuFgKey, cpuDurationForeground) - .setConsumedPower(cpuBgKey, cpuPowerBackground, + .setUsageDurationMillis(cpuBatScrOn, cpuDurationBatScrOn) + .setConsumedPower(cpuBatScrOff, cpuPowerBatScrOff, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuBgKey, cpuDurationBackground) - .setConsumedPower(cpuFgsKey, cpuPowerFgs, + .setUsageDurationMillis(cpuBatScrOff, cpuDurationBatScrOff) + .setConsumedPower(cpuChgScrOn, cpuPowerChgScrOn, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuFgsKey, cpuDurationFgs) - .setConsumedPower(cpuCachedKey, cpuPowerCached, + .setUsageDurationMillis(cpuChgScrOn, cpuDurationChgScrOn) + .setConsumedPower(cpuChgScrOff, cpuPowerChgScrOff, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuCachedKey, cpuDurationCached); + .setUsageDurationMillis(cpuChgScrOff, cpuDurationChgScrOff); } } @@ -456,7 +511,7 @@ public class BatteryUsageStatsTest { for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) { if (uidBatteryConsumer.getUid() == APP_UID1) { assertUidBatteryConsumer(uidBatteryConsumer, 1200, "foo", - 1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400, + 1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5787, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 500, 600, 800, true, 1777, 1888, 1999, 123, 1777, 7771, 1888, 8881, 1999, 9991, 123, 456); @@ -568,54 +623,53 @@ public class BatteryUsageStatsTest { .isEqualTo(totalPowerCached); } - final BatteryConsumer.Key cpuFgKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cpuFg = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_FOREGROUND); if (processStateDataIncluded) { - assertThat(cpuFgKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cpuFgKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cpuFg)) .isEqualTo(cpuPowerForeground); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFg)) .isEqualTo(cpuDurationForeground); } else { - assertThat(cpuFgKey).isNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cpuFg)).isEqualTo(0); } - final BatteryConsumer.Key cpuBgKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cpuBg = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_BACKGROUND); if (processStateDataIncluded) { - assertThat(cpuBgKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cpuBgKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cpuBg)) .isEqualTo(cpuPowerBackground); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBgKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBg)) .isEqualTo(cpuDurationBackground); } else { - assertThat(cpuBgKey).isNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cpuBg)) + .isEqualTo(0); } - final BatteryConsumer.Key cpuFgsKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cpuFgs = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE); if (processStateDataIncluded) { - assertThat(cpuFgsKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cpuFgsKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cpuFgs)) .isEqualTo(cpuPowerFgs); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgsKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgs)) .isEqualTo(cpuDurationFgs); } else { - assertThat(cpuFgsKey).isNotNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cpuFgs)) + .isEqualTo(0); } - final BatteryConsumer.Key cachedKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cached = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_CACHED); if (processStateDataIncluded) { - assertThat(cachedKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cachedKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cached)) .isEqualTo(cpuPowerCached); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cachedKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cached)) .isEqualTo(cpuDurationCached); } else { - assertThat(cpuFgsKey).isNotNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cached)) + .isEqualTo(0); } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java index 644ae4717eb1..005ceee703a8 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java @@ -130,7 +130,7 @@ public class CpuPowerStatsCollectorValidationTest { boolean inCpuSection = false; for (int i = 0; i < lines.length; i++) { if (!inCpuSection) { - if (lines[i].startsWith("CpuPowerStatsCollector")) { + if (lines[i].startsWith("cpu (1)")) { inCpuSection = true; } } else if (lines[i].startsWith(" ")) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java index 7bec13f653e7..1621d47d62b5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java @@ -149,9 +149,9 @@ public class CustomEnergyConsumerPowerStatsTest { .isEqualTo(20000); assertThat(ps2.uidStats.size()).isEqualTo(2); assertThat(POWER_STATS_LAYOUT.getUidConsumedEnergy(ps2.uidStats.get(APP_UID1), 0)) - .isEqualTo(14000); + .isEqualTo(4000); assertThat(POWER_STATS_LAYOUT.getUidConsumedEnergy(ps2.uidStats.get(APP_UID2), 0)) - .isEqualTo(21000); + .isEqualTo(6000); } @Test @@ -209,8 +209,8 @@ public class CustomEnergyConsumerPowerStatsTest { assertThat(POWER_STATS_LAYOUT.getDevicePowerEstimate(deviceStats)) .isWithin(PRECISION).of(expectedPower * 0.75); - // UID1: estimated power = 14,000 uC = 0.00388 mAh - expectedPower = 0.00388; + // UID1: estimated power = 4,000 uC = 0.00111 mAh + expectedPower = 0.00111; ps2.getUidStats(uidStats, APP_UID1, states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND)); assertThat(POWER_STATS_LAYOUT.getUidPowerEstimate(uidStats)) @@ -221,8 +221,8 @@ public class CustomEnergyConsumerPowerStatsTest { assertThat(POWER_STATS_LAYOUT.getUidPowerEstimate(uidStats)) .isWithin(PRECISION).of(expectedPower * 0.75); - // UID2: estimated power = 21,000 uC = 0.00583 mAh - expectedPower = 0.00583; + // UID2: estimated power = 6,000 uC = 0.00166 mAh + expectedPower = 0.00167; ps2.getUidStats(uidStats, APP_UID2, states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); assertThat(POWER_STATS_LAYOUT.getUidPowerEstimate(uidStats)) diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java index 32bfb2cd3507..7f7967ba4d7b 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java @@ -19,7 +19,6 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import android.os.AggregateBatteryConsumer; @@ -131,9 +130,20 @@ public class PowerStatsExporterTest { @Test public void breakdownByProcState_fullRange() throws Exception { + breakdownByProcState_fullRange(false, false); + } + + @Test + public void breakdownByProcStateScreenAndPower_fullRange() throws Exception { + breakdownByProcState_fullRange(true, true); + } + + private void breakdownByProcState_fullRange(boolean includeScreenStateData, + boolean includePowerStateData) throws Exception { BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( new String[]{"cu570m"}, /* includePowerModels */ false, - /* includeProcessStateData */ true, /* powerThreshold */ 0); + /* includeProcessStateData */ true, includeScreenStateData, + includePowerStateData, /* powerThreshold */ 0); exportAggregatedPowerStats(builder, 1000, 10000); BatteryUsageStats actual = builder.build(); @@ -177,7 +187,7 @@ public class PowerStatsExporterTest { public void breakdownByProcState_subRange() throws Exception { BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( new String[]{"cu570m"}, /* includePowerModels */ false, - /* includeProcessStateData */ true, /* powerThreshold */ 0); + /* includeProcessStateData */ true, true, true, /* powerThreshold */ 0); exportAggregatedPowerStats(builder, 3700, 6700); BatteryUsageStats actual = builder.build(); @@ -209,7 +219,7 @@ public class PowerStatsExporterTest { public void combinedProcessStates() throws Exception { BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( new String[]{"cu570m"}, /* includePowerModels */ false, - /* includeProcessStateData */ false, /* powerThreshold */ 0); + /* includeProcessStateData */ false, true, true, /* powerThreshold */ 0); exportAggregatedPowerStats(builder, 1000, 10000); BatteryUsageStats actual = builder.build(); @@ -229,13 +239,13 @@ public class PowerStatsExporterTest { UidBatteryConsumer uidScope = actual.getUidBatteryConsumers().stream() .filter(us -> us.getUid() == APP_UID1).findFirst().orElse(null); // There shouldn't be any per-procstate data - assertThrows( - IllegalArgumentException.class, - () -> uidScope.getConsumedPower(new BatteryConsumer.Dimensions( + for (int procState = 0; procState < BatteryConsumer.PROCESS_STATE_COUNT; procState++) { + if (procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { + assertThat(uidScope.getConsumedPower(new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND))); - - + BatteryConsumer.PROCESS_STATE_FOREGROUND))).isEqualTo(0); + } + } actual.close(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index 758c84a26dcd..ef9580c54de6 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -101,7 +101,7 @@ public class AbsoluteVolumeBehaviorTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)) { + mock(AudioServerPermissionProvider.class), r -> r.run()) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 2cb02bdd2806..464515632997 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -78,7 +78,7 @@ public class AudioDeviceVolumeManagerTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)) { + mock(AudioServerPermissionProvider.class), r -> r.run()) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index 037c3c00443c..b7100ea00a40 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -87,7 +87,7 @@ public class AudioServiceTest { .thenReturn(AppOpsManager.MODE_ALLOWED); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null, - mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider); + mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider, r -> r.run()); } /** diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 27b552fa7cdd..746645a8c585 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -78,7 +78,7 @@ public class DeviceVolumeBehaviorTest { mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)); + mock(AudioServerPermissionProvider.class), r -> r.run()); mTestLooper.dispatchAll(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 8e34ee1b6a42..e45ab319146c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -160,7 +160,7 @@ public class VolumeHelperTest { @NonNull PermissionEnforcer enforcer, AudioServerPermissionProvider permissionProvider) { super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, appOps, enforcer, permissionProvider); + audioPolicy, looper, appOps, enforcer, permissionProvider, r -> r.run()); } public void setDeviceForStream(int stream, int device) { diff --git a/services/tests/servicestests/src/com/android/server/autofill/HelperTest.java b/services/tests/servicestests/src/com/android/server/autofill/HelperTest.java new file mode 100644 index 000000000000..f698bea1a199 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/autofill/HelperTest.java @@ -0,0 +1,113 @@ +/* + * 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.server.autofill; + +import static com.android.server.autofill.Helper.SaveInfoStats; +import static com.android.server.autofill.Helper.getSaveInfoStatsFromFillResponses; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; +import android.service.autofill.FillResponse; +import android.service.autofill.SaveInfo; +import android.util.SparseArray; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class HelperTest { + + @Test + public void testGetSaveInfoStatsFromFillResponses_nullFillResponses() { + SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(null); + + assertThat(saveInfoStats.saveInfoCount).isEqualTo(-1); + assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(-1); + } + + @Test + public void testGetSaveInfoStatsFromFillResponses_emptyFillResponseSparseArray() { + SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(new SparseArray<>()); + + assertThat(saveInfoStats.saveInfoCount).isEqualTo(0); + assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(0); + } + + @Test + public void testGetSaveInfoStatsFromFillResponses_singleResponseWithoutSaveInfo() { + FillResponse.Builder fillResponseBuilder = new FillResponse.Builder(); + // Add client state to satisfy the sanity check in FillResponseBuilder.build() + Bundle clientState = new Bundle(); + fillResponseBuilder.setClientState(clientState); + FillResponse testFillResponse = fillResponseBuilder.build(); + + SparseArray<FillResponse> testFillResponses = new SparseArray<>(); + testFillResponses.put(0, testFillResponse); + + SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(testFillResponses); + + assertThat(saveInfoStats.saveInfoCount).isEqualTo(0); + assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(0); + } + + @Test + public void testGetSaveInfoStatsFromFillResponses_singleResponseWithSaveInfo() { + FillResponse.Builder fillResponseBuilder = new FillResponse.Builder(); + SaveInfo.Builder saveInfoBuilder = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC); + fillResponseBuilder.setSaveInfo(saveInfoBuilder.build()); + FillResponse testFillResponse = fillResponseBuilder.build(); + + SparseArray<FillResponse> testFillResponses = new SparseArray<>(); + testFillResponses.put(0, testFillResponse); + + SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(testFillResponses); + + assertThat(saveInfoStats.saveInfoCount).isEqualTo(1); + assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(1); + } + + @Test + public void testGetSaveInfoStatsFromFillResponses_multipleResponseWithDifferentTypeSaveInfo() { + FillResponse.Builder fillResponseBuilder1 = new FillResponse.Builder(); + SaveInfo.Builder saveInfoBuilder1 = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC); + fillResponseBuilder1.setSaveInfo(saveInfoBuilder1.build()); + FillResponse testFillResponse1 = fillResponseBuilder1.build(); + + FillResponse.Builder fillResponseBuilder2 = new FillResponse.Builder(); + SaveInfo.Builder saveInfoBuilder2 = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS); + fillResponseBuilder2.setSaveInfo(saveInfoBuilder2.build()); + FillResponse testFillResponse2 = fillResponseBuilder2.build(); + + FillResponse.Builder fillResponseBuilder3 = new FillResponse.Builder(); + SaveInfo.Builder saveInfoBuilder3 = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS); + fillResponseBuilder3.setSaveInfo(saveInfoBuilder3.build()); + FillResponse testFillResponse3 = fillResponseBuilder3.build(); + + SparseArray<FillResponse> testFillResponses = new SparseArray<>(); + testFillResponses.put(0, testFillResponse1); + testFillResponses.put(1, testFillResponse2); + testFillResponses.put(2, testFillResponse3); + + SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(testFillResponses); + + // Save info count is 3. Since two save info share the same save data type, the distinct + // save data type count is 2. + assertThat(saveInfoStats.saveInfoCount).isEqualTo(3); + assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(2); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 0f385325e525..a4222ff5650b 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -1518,7 +1518,8 @@ public class BiometricServiceTest { mBiometricService.onStart(); when(mTrustManager.isInSignificantPlace()).thenReturn(false); - when(mBiometricService.mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt())) + when(mBiometricService.mSettingObserver + .getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(anyInt())) .thenReturn(true); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); @@ -1540,7 +1541,8 @@ public class BiometricServiceTest { mBiometricService.onStart(); when(mTrustManager.isInSignificantPlace()).thenReturn(false); - when(mBiometricService.mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt())) + when(mBiometricService.mSettingObserver + .getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(anyInt())) .thenReturn(true); setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); @@ -1564,7 +1566,8 @@ public class BiometricServiceTest { mBiometricService.onStart(); when(mTrustManager.isInSignificantPlace()).thenReturn(false); - when(mBiometricService.mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt())) + when(mBiometricService.mSettingObserver + .getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(anyInt())) .thenReturn(true); setupAuthForOnly(TYPE_CREDENTIAL, Authenticators.DEVICE_CREDENTIAL); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java index b831ef5c30bc..240da9fe46bd 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -90,7 +90,8 @@ public class PreAuthInfoTest { when(mDevicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) .thenReturn(KEYGUARD_DISABLE_FEATURES_NONE); when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); - when(mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt())).thenReturn(true); + when(mSettingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser( + anyInt())).thenReturn(true); when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java index 8a9538f2374a..ebdde94237eb 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java @@ -66,7 +66,7 @@ public class NetworkEventTest extends DpmTestBase { any(UserHandle.class)); mDpmTestable = new DevicePolicyManagerServiceTestable(getServices(), mSpiedDpmMockContext); setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_UID); - mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE); + mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE, null); } @Test diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 316b5faf2a68..689b241f0faa 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -364,6 +365,39 @@ public class MediaProjectionManagerServiceTest { @EnableFlags(android.companion.virtualdevice.flags .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test + public void testReuseProjection_keyguardNotLocked_startConsentDialog() + throws NameNotFoundException { + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + projection.start(mIMediaProjectionCallback); + + doNothing().when(mContext).startActivityAsUser(any(), any()); + doReturn(false).when(mKeyguardManager).isKeyguardLocked(); + + MediaProjectionManagerService.BinderService mediaProjectionBinderService = + mService.new BinderService(mContext); + mediaProjectionBinderService.requestConsentForInvalidProjection(projection); + + verify(mContext).startActivityAsUser(any(), any()); + } + + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test + public void testReuseProjection_keyguardLocked_noConsentDialog() throws NameNotFoundException { + MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); + projection.start(mIMediaProjectionCallback); + + doReturn(true).when(mKeyguardManager).isKeyguardLocked(); + MediaProjectionManagerService.BinderService mediaProjectionBinderService = + mService.new BinderService(mContext); + mediaProjectionBinderService.requestConsentForInvalidProjection(projection); + + verify(mContext, never()).startActivityAsUser(any(), any()); + } + + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test public void testKeyguardLocked_stopsActiveProjection() throws Exception { MediaProjectionManagerService service = new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector); diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java index 54d11387752c..cbf79356e9c4 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java +++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java @@ -16,7 +16,6 @@ package com.android.server.webkit; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -66,10 +65,12 @@ public class TestSystemImpl implements SystemInterface { } @Override - public String getUserChosenWebViewProvider(Context context) { return mUserProvider; } + public String getUserChosenWebViewProvider() { + return mUserProvider; + } @Override - public void updateUserSetting(Context context, String newProviderName) { + public void updateUserSetting(String newProviderName) { mUserProvider = newProviderName; } @@ -77,14 +78,14 @@ public class TestSystemImpl implements SystemInterface { public void killPackageDependents(String packageName) {} @Override - public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { + public void enablePackageForAllUsers(String packageName, boolean enable) { for(int userId : mUsers) { enablePackageForUser(packageName, enable, userId); } } @Override - public void installExistingPackageForAllUsers(Context context, String packageName) { + public void installExistingPackageForAllUsers(String packageName) { for (int userId : mUsers) { installPackageForUser(packageName, userId); } @@ -131,8 +132,7 @@ public class TestSystemImpl implements SystemInterface { } @Override - public List<UserPackage> getPackageInfoForProviderAllUsers( - Context context, WebViewProviderInfo info) { + public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo info) { Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName); List<UserPackage> ret = new ArrayList(); // Loop over defined users, and find the corresponding package for each user. @@ -185,12 +185,12 @@ public class TestSystemImpl implements SystemInterface { } @Override - public int getMultiProcessSetting(Context context) { + public int getMultiProcessSetting() { return mMultiProcessSetting; } @Override - public void setMultiProcessSetting(Context context, int value) { + public void setMultiProcessSetting(int value) { mMultiProcessSetting = value; } diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java index e181a513b637..06479c84bfc7 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java @@ -104,10 +104,10 @@ public class WebViewUpdateServiceTest { mTestSystemImpl = Mockito.spy(testing); if (updateServiceV2()) { mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl); + new WebViewUpdateServiceImpl2(mTestSystemImpl); } else { mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl); + new WebViewUpdateServiceImpl(mTestSystemImpl); } } @@ -140,7 +140,7 @@ public class WebViewUpdateServiceTest { WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) { setupWithPackagesAndRelroCount(webviewPackages, numRelros); if (userSetting != null) { - mTestSystemImpl.updateUserSetting(null, userSetting); + mTestSystemImpl.updateUserSetting(userSetting); } // Add (enabled and valid) package infos for each provider setEnabledAndValidPackageInfos(webviewPackages); @@ -313,7 +313,7 @@ public class WebViewUpdateServiceTest { }; setupWithPackagesNonDebuggable(packages); // Start with the setting pointing to the invalid package - mTestSystemImpl.updateUserSetting(null, invalidPackage); + mTestSystemImpl.updateUserSetting(invalidPackage); mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */, true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature} , 0 /* updateTime */)); @@ -481,7 +481,7 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo(secondPackage, "", true, false, null)}; setupWithPackages(packages); // Start with the setting pointing to the second package - mTestSystemImpl.updateUserSetting(null, secondPackage); + mTestSystemImpl.updateUserSetting(secondPackage); // Have all packages be enabled, so that we can change provider however we want to setEnabledAndValidPackageInfos(packages); @@ -572,7 +572,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic re-enables the fallback package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl).enablePackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + Mockito.eq(testPackage), Mockito.eq(true)); // Fake the message about the enabling having changed the package state, // and check we now use that package. @@ -657,7 +657,7 @@ public class WebViewUpdateServiceTest { null)}; setupWithPackages(packages); // Start with the setting pointing to the secondary package - mTestSystemImpl.updateUserSetting(null, secondaryPackage); + mTestSystemImpl.updateUserSetting(secondaryPackage); int secondaryUserId = 10; int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID; if (multiUser) { @@ -710,7 +710,7 @@ public class WebViewUpdateServiceTest { null)}; setupWithPackages(packages); // Start with the setting pointing to the secondary package - mTestSystemImpl.updateUserSetting(null, secondaryPackage); + mTestSystemImpl.updateUserSetting(secondaryPackage); setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); int newUser = 100; mTestSystemImpl.addUser(newUser); @@ -832,14 +832,13 @@ public class WebViewUpdateServiceTest { true /* installed */)); // Set user-chosen package - mTestSystemImpl.updateUserSetting(null, chosenPackage); + mTestSystemImpl.updateUserSetting(chosenPackage); runWebViewBootPreparationOnMainSync(); // Verify that we switch the setting to point to the current package - Mockito.verify(mTestSystemImpl).updateUserSetting( - Mockito.anyObject(), Mockito.eq(nonChosenPackage)); - assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider(null)); + Mockito.verify(mTestSystemImpl).updateUserSetting(Mockito.eq(nonChosenPackage)); + assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider()); checkPreparationPhasesForPackage(nonChosenPackage, 1); } @@ -976,7 +975,7 @@ public class WebViewUpdateServiceTest { setEnabledAndValidPackageInfos(packages); // Start with the setting pointing to the third package - mTestSystemImpl.updateUserSetting(null, thirdPackage); + mTestSystemImpl.updateUserSetting(thirdPackage); runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(thirdPackage, 1); @@ -1167,7 +1166,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 5; if (multiUser) { mTestSystemImpl.addUser(secondaryUserId); @@ -1220,7 +1219,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 412; mTestSystemImpl.addUser(secondaryUserId); @@ -1277,7 +1276,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 4; mTestSystemImpl.addUser(secondaryUserId); @@ -1290,7 +1289,7 @@ public class WebViewUpdateServiceTest { 0 /* updateTime */, (testHidden ? true : false) /* hidden */)); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); runWebViewBootPreparationOnMainSync(); @@ -1458,7 +1457,7 @@ public class WebViewUpdateServiceTest { runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */); - mTestSystemImpl.setMultiProcessSetting(null /* context */, settingValue); + mTestSystemImpl.setMultiProcessSetting(settingValue); assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); } @@ -1492,7 +1491,7 @@ public class WebViewUpdateServiceTest { }; setupWithPackages(packages); // Start with the setting pointing to the invalid package - mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName); + mTestSystemImpl.updateUserSetting(oldSdkPackage.packageName); mTestSystemImpl.setPackageInfo(newSdkPackage); mTestSystemImpl.setPackageInfo(currentSdkPackage); @@ -1545,8 +1544,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic re-enables the default package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl) - .enablePackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + .enablePackageForAllUsers(Mockito.eq(testPackage), Mockito.eq(true)); } @Test @@ -1570,8 +1568,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic tries to install the default package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } @Test @@ -1598,8 +1595,7 @@ public class WebViewUpdateServiceTest { // Check that we try to re-install the default package. Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } /** @@ -1632,8 +1628,7 @@ public class WebViewUpdateServiceTest { // Check that we try to re-install the default package for all users. Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } private void testDefaultPackageChosen(PackageInfo packageInfo) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 4bbbc2b28dad..b07940ad8de3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -186,6 +186,9 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.xmlpull.v1.XmlPullParserException; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -205,9 +208,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @RunWith(ParameterizedAndroidJunit4.class) @@ -5022,6 +5022,34 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MODES_API) + public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule_forWatch() { + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); + AutomaticZenRule rule = + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String ruleId = + mZenModeHelper.addAutomaticZenRule( + mPkg, rule, UPDATE_ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState( + ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + AutomaticZenRule updateWithDiff = + new AutomaticZenRule.Builder(rule).setTriggerDescription("Whenever").build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_USER, "reason", + CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( + CONDITION_TRUE); + } + + @Test @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) public void updateAutomaticZenRule_ruleDisabledByUser_doesNotReactivateOnReenable() { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 9dac23f075e6..d7004e72bc52 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -1746,10 +1746,6 @@ public class VibrationThreadTest { assertTrue("Tested duration=" + duration4, duration4 < 2000); // Effect5: played normally after effect4, which may or may not have played. - - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId5)); - verifyCallbacksTriggered(vibrationId5, Vibration.Status.FINISHED); - assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), fakeVibrator.getEffectSegments(vibrationId5)); } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index ef944dbba3ca..5ae5677b9b53 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -47,6 +47,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Resources; @@ -103,6 +104,7 @@ import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; +import com.android.server.pm.BackgroundUserSoundNotifier; import org.junit.After; import org.junit.Before; @@ -809,6 +811,32 @@ public class VibratorManagerServiceTest { } @Test + @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS) + public void vibrate_thenFgUserRequestsMute_getsCancelled() throws Throwable { + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + + var vib = vibrate(service, + VibrationEffect.createWaveform(new long[]{100, 100, 100, 100}, 0), ALARM_ATTRS); + + assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + + + service.mIntentReceiver.onReceive(mContextSpy, new Intent( + BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)); + + assertTrue(waitUntil(s -> vib.hasEnded(), service, TEST_TIMEOUT_MILLIS)); + + var statsInfoCaptor = ArgumentCaptor.forClass(VibrationStats.StatsInfo.class); + verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS)) + .writeVibrationReportedAsync(statsInfoCaptor.capture()); + + VibrationStats.StatsInfo touchMetrics = statsInfoCaptor.getAllValues().get(0); + assertEquals(Vibration.Status.CANCELLED_BY_FOREGROUND_USER.getProtoEnumValue(), + touchMetrics.status); + } + + @Test public void vibrate_withVibrationAttributes_usesCorrespondingAudioUsageInAppOpsManager() { VibratorManagerService service = createSystemReadyService(); diff --git a/services/tests/wmtests/res/xml/bookmarks.xml b/services/tests/wmtests/res/xml/bookmarks.xml index 88419e9c441b..1549b2de78c3 100644 --- a/services/tests/wmtests/res/xml/bookmarks.xml +++ b/services/tests/wmtests/res/xml/bookmarks.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2024 The Android Open Source Project +<!-- 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. @@ -14,6 +14,8 @@ limitations under the License. --> <bookmarks> + <!-- the key combinations for the following shortcuts must be in sync + with the key combinations sent by the test in ModifierShortcutTests.java --> <bookmark role="android.app.role.BROWSER" shortcut="b" /> @@ -38,4 +40,37 @@ <bookmark category="android.intent.category.APP_CALCULATOR" shortcut="u" /> + + <!-- The following shortcuts will not be invoked by tests but are here to + provide test coverage of parsing the different types of shortcut. --> + <bookmark + package="com.test" + class="com.test.BookmarkTest" + shortcut="a" /> + <bookmark + package="com.test2" + class="com.test.BookmarkTest" + shortcut="d" /> + + <bookmark + role="android.app.role.BROWSER" + shortcut="b" + shift="true" /> + <bookmark + category="android.intent.category.APP_CONTACTS" + shortcut="c" + shift="true" /> + <bookmark + package="com.test" + class="com.test.BookmarkTest" + shortcut="a" + shift="true" /> + + <!-- It's intended that this package/class will NOT resolve so we test the resolution + failure case. --> + <bookmark + package="com.test3" + class="com.test.BookmarkTest" + shortcut="f" /> + </bookmarks> diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java index 896edffee875..1c331166d317 100644 --- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java @@ -24,8 +24,8 @@ import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAV import android.view.ViewConfiguration; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java index 8c375d413950..5533ff909c50 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * 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. @@ -19,15 +19,22 @@ package com.android.server.policy; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static org.junit.Assert.assertEquals; +import static org.mockito.AdditionalMatchers.aryEq; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyObject; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.os.Handler; @@ -58,27 +65,56 @@ public class ModifierShortcutManagerTests { private Handler mHandler; private Context mContext; private Resources mResources; + private PackageManager mPackageManager; @Before public void setUp() { mHandler = new Handler(Looper.getMainLooper()); mContext = spy(getInstrumentation().getTargetContext()); mResources = spy(mContext.getResources()); + mPackageManager = spy(mContext.getPackageManager()); XmlResourceParser testBookmarks = mResources.getXml( com.android.frameworks.wmtests.R.xml.bookmarks); when(mContext.getResources()).thenReturn(mResources); + when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mResources.getXml(R.xml.bookmarks)).thenReturn(testBookmarks); + try { + // Keep packageName / className in sync with + // services/tests/wmtests/res/xml/bookmarks.xml + ActivityInfo testActivityInfo = new ActivityInfo(); + testActivityInfo.applicationInfo = new ApplicationInfo(); + testActivityInfo.packageName = + testActivityInfo.applicationInfo.packageName = "com.test"; + + doReturn(testActivityInfo).when(mPackageManager).getActivityInfo( + eq(new ComponentName("com.test", "com.test.BookmarkTest")), anyInt()); + doThrow(new PackageManager.NameNotFoundException("com.test3")).when(mPackageManager) + .getActivityInfo(eq(new ComponentName("com.test3", "com.test.BookmarkTest")), + anyInt()); + } catch (PackageManager.NameNotFoundException ignored) { } + doReturn(new String[] { "com.test" }).when(mPackageManager) + .canonicalToCurrentPackageNames(aryEq(new String[] { "com.test2" })); + mModifierShortcutManager = new ModifierShortcutManager(mContext, mHandler); } @Test public void test_getApplicationLaunchKeyboardShortcuts() { + // Expected values here determined by the number of shortcuts defined in + // services/tests/wmtests/res/xml/bookmarks.xml + + // Total valid shortcuts. KeyboardShortcutGroup group = mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(-1); - assertEquals(8, group.getItems().size()); + assertEquals(13, group.getItems().size()); + + // Total valid shift shortcuts. + assertEquals(3, group.getItems().stream() + .filter(s -> s.getModifiers() == (KeyEvent.META_SHIFT_ON | KeyEvent.META_META_ON)) + .count()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index b4505fad1b20..24fc7ee0c392 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2955,7 +2955,8 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testStartingWindowInTaskFragment() { - final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true) + .setVisible(false).build(); final WindowState startingWindow = createWindowState( new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1); activity1.addWindow(startingWindow); @@ -3011,6 +3012,28 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test + public void testStartingWindowInTaskFragmentWithVisibleTask() { + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task task = activity1.getTask(); + final Rect taskBounds = task.getBounds(); + final Rect tfBounds = new Rect(taskBounds.left, taskBounds.top, + taskBounds.left + taskBounds.width() / 2, taskBounds.bottom); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).setParentTask(task) + .setBounds(tfBounds).build(); + + final ActivityRecord activity2 = new ActivityBuilder(mAtm).build(); + final WindowState startingWindow = createWindowState( + new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1); + taskFragment.addChild(activity2); + activity2.addWindow(startingWindow); + activity2.mStartingData = mock(StartingData.class); + activity2.attachStartingWindow(startingWindow); + + assertNull(activity2.mStartingData.mAssociatedTask); + assertNull(task.mSharedStartingData); + } + + @Test public void testTransitionAnimationBounds() { removeGlobalMinSizeRestriction(); final Task task = new TaskBuilder(mSupervisor) diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java new file mode 100644 index 000000000000..ddd6d562abb1 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java @@ -0,0 +1,316 @@ +/* + * 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.server.wm; + +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; +import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2; +import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertEquals; + +import android.compat.testing.PlatformCompatChangeRule; +import android.platform.test.annotations.Presubmit; + +import androidx.annotation.NonNull; + +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; + +/** + * Test class for {@link AppCompatAspectRatioOverrides}. + * <p> + * Build/Install/Run: + * atest WmTests:AppCompatAspectRatioOverridesTest + */ +@Presubmit +@RunWith(WindowTestRunner.class) +public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + @Test + public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() { + runTestScenario((robot)-> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE); + robot.conf().enableUserAppAspectRatioFullscreen(/* enabled */ false); + + robot.activity().createActivityWithComponent(); + + robot.checkShouldApplyUserFullscreenOverride(/* expected */ false); + }); + } + + @Test + public void testShouldApplyUserFullscreenOverride_falseFullscreenProperty_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioFullscreen(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN); + + robot.checkShouldApplyUserFullscreenOverride(/* expected */ false); + }); + } + + @Test + public void testShouldApplyUserFullscreenOverride_falseSettingsProperty_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN); + robot.checkShouldApplyUserFullscreenOverride(/* expected */ false); + }); + } + + + @Test + public void testShouldApplyUserFullscreenOverride_returnsTrue() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioFullscreen(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN); + + robot.checkShouldApplyUserFullscreenOverride(/* expected */ true); + }); + } + + @Test + public void testShouldEnableUserAspectRatioSettings_falseProperty_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldEnableUserAspectRatioSettings(/* expected */ false); + }); + } + + @Test + public void testShouldEnableUserAspectRatioSettings_trueProperty_returnsTrue() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldEnableUserAspectRatioSettings(/* expected */ true); + }); + } + + @Test + public void testShouldEnableUserAspectRatioSettings_ignoreOrientation_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ false); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldEnableUserAspectRatioSettings(/* expected */ false); + }); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_falseProperty_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldEnableUserAspectRatioSettings(/* expected */ false); + }); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_trueProperty_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ false); + robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + + robot.checkShouldEnableUserAspectRatioSettings(/* enabled */ false); + }); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_disabledIgnoreOrientationRequest() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ false); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldApplyUserMinAspectRatioOverride(/* expected */ false); + }); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_returnsTrue() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldApplyUserMinAspectRatioOverride(/* expected */ true); + }); + } + + @Test + public void testShouldApplyUserMinAspectRatioOverride_ignoreOrientation_returnsFalse() { + runTestScenario((robot)-> { + robot.conf().enableUserAppAspectRatioSettings(/* enabled */ false); + robot.activity().setIgnoreOrientationRequest(/* enabled */ true); + robot.activity().createActivityWithComponent(); + robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2); + + robot.checkShouldApplyUserMinAspectRatioOverride(/* expected */ false); + }); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testShouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() { + runTestScenario((robot)-> { + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatio(/* expected */ true); + }); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testShouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue() { + runTestScenario((robot)-> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatio(/* expected */ true); + }); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testShouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse() { + runTestScenario((robot)-> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatio(/* expected */ false); + }); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testShouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() { + runTestScenario((robot)-> { + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatio(/* expected */ false); + }); + } + + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse() { + runTestScenario((robot)-> { + robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatio(/* expected */ false); + }); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse() { + runTestScenario((robot)-> { + robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatio(/* expected */ false); + }); + } + + /** + * Runs a test scenario providing a Robot. + */ + void runTestScenario(@NonNull Consumer<AspectRatioOverridesRobotTest> consumer) { + spyOn(mWm.mAppCompatConfiguration); + final AspectRatioOverridesRobotTest robot = + new AspectRatioOverridesRobotTest(mWm, mAtm, mSupervisor); + consumer.accept(robot); + } + + private static class AspectRatioOverridesRobotTest extends AppCompatRobotBase { + + AspectRatioOverridesRobotTest(@NonNull WindowManagerService wm, + @NonNull ActivityTaskManagerService atm, + @NonNull ActivityTaskSupervisor supervisor) { + super(wm, atm, supervisor); + } + + void checkShouldApplyUserFullscreenOverride(boolean expected) { + assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides() + .shouldApplyUserFullscreenOverride()); + } + + void checkShouldEnableUserAspectRatioSettings(boolean expected) { + assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides() + .shouldEnableUserAspectRatioSettings()); + } + + void checkShouldApplyUserMinAspectRatioOverride(boolean expected) { + assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride()); + } + + void checkShouldOverrideMinAspectRatio(boolean expected) { + assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides() + .shouldOverrideMinAspectRatio()); + } + + @NonNull + private AppCompatAspectRatioOverrides getTopActivityAppCompatAspectRatioOverrides() { + return activity().top().mAppCompatController.getAppCompatAspectRatioOverrides(); + } + } + +} diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java index d8c7fb3594c1..de99f546ab07 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java @@ -25,6 +25,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAME import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; @@ -36,6 +37,7 @@ import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; import org.junit.Rule; @@ -286,6 +288,88 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { }); } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_overrideEnabled_returnsTrue() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.activity().activateCameraInPolicy(/* isCameraActive */ true); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ true); + }); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsTrue() { + runTestScenario((robot) -> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().activateCameraInPolicy(/* isCameraActive */ true); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ true); + }); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsFalse() { + runTestScenario((robot) -> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().activateCameraInPolicy(/* isCameraActive */ false); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false); + }); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideDisabled_returnsFalse() { + runTestScenario((robot) -> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().activateCameraInPolicy(/* isCameraActive */ true); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false); + }); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_overrideDisabled_returnsFalse() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.activity().activateCameraInPolicy(/* isCameraActive */ true); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false); + }); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyFalse_overrideEnabled_returnsFalse() { + runTestScenario((robot) -> { + robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false); + }); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) + public void shouldOverrideMinAspectRatioForCamera_propertyFalse_noOverride_returnsFalse() { + runTestScenario((robot) -> { + robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + robot.activity().createActivityWithComponent(); + robot.activity().activateCameraInPolicy(/* isCameraActive */ true); + + robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false); + }); + } + /** * Runs a test scenario providing a Robot. */ @@ -323,6 +407,11 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { .shouldApplyFreeformTreatmentForCameraCompat(), expected); } + void checkShouldOverrideMinAspectRatioForCamera(boolean expected) { + Assert.assertEquals(getAppCompatCameraOverrides() + .shouldOverrideMinAspectRatioForCamera(), expected); + } + void checkIsCameraActive(boolean active) { Assert.assertEquals(getAppCompatCameraOverrides().isCameraActive(), active); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java index d568eecfd1c5..361177f480a6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java @@ -32,27 +32,27 @@ import androidx.annotation.NonNull; */ class AppCompatComponentPropRobot { @NonNull - private final WindowManagerService mWm; + private final PackageManager mPackageManager; AppCompatComponentPropRobot(@NonNull WindowManagerService wm) { - mWm = wm; + mPackageManager = wm.mContext.getPackageManager(); + spyOn(mPackageManager); } void enable(@NonNull String propertyName) { - setPropertyValue(propertyName, /* enabled */ true); + setPropertyValue(propertyName, "", "", /* enabled */ true); } void disable(@NonNull String propertyName) { - setPropertyValue(propertyName, /* enabled */ false); + setPropertyValue(propertyName, "", "", /* enabled */ false); } - private void setPropertyValue(@NonNull String propertyName, boolean enabled) { + private void setPropertyValue(@NonNull String propertyName, @NonNull String packageName, + @NonNull String className, boolean enabled) { final PackageManager.Property property = new PackageManager.Property(propertyName, - /* value */ enabled, /* packageName */ "", /* className */ ""); - final PackageManager pm = mWm.mContext.getPackageManager(); - spyOn(pm); + /* value */ enabled, packageName, className); try { - doReturn(property).when(pm).getProperty(eq(propertyName), anyString()); + doReturn(property).when(mPackageManager).getProperty(eq(propertyName), anyString()); } catch (PackageManager.NameNotFoundException e) { fail(e.getLocalizedMessage()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java index cb3cf6bd2a5c..0a1b16bfc3e9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java @@ -66,6 +66,4 @@ class AppCompatConfigurationRobot { doReturn(enabled).when(mAppCompatConfiguration) .isCameraCompatSplitScreenAspectRatioEnabled(); } - - } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 63c14b90958f..afa22bc5eae8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -67,6 +67,7 @@ import android.window.IOnBackInvokedCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedCallbackInfo; import android.window.OnBackInvokedDispatcher; +import android.window.TaskFragmentOrganizer; import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; @@ -670,25 +671,29 @@ public class BackNavigationControllerTests extends WindowTestsBase { } @Test - public void testAdjacentFocusInActivityEmbedding() { + public void testBackOnMostRecentWindowInActivityEmbedding() { mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG); final Task task = createTask(mDefaultDisplay); - final TaskFragment primaryTf = createTaskFragmentWithActivity(task); - final TaskFragment secondaryTf = createTaskFragmentWithActivity(task); + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final TaskFragment primaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment secondaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer); final ActivityRecord primaryActivity = primaryTf.getTopMostActivity(); final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity(); primaryTf.setAdjacentTaskFragment(secondaryTf); secondaryTf.setAdjacentTaskFragment(primaryTf); - final WindowState windowState = mock(WindowState.class); - windowState.mActivityRecord = primaryActivity; - doReturn(windowState).when(mWm).getFocusedWindowLocked(); - doReturn(primaryTf).when(windowState).getTaskFragment(); + final WindowState primaryWindow = mock(WindowState.class); + final WindowState secondaryWindow = mock(WindowState.class); + doReturn(primaryActivity).when(primaryWindow).getActivityRecord(); + doReturn(secondaryActivity).when(secondaryWindow).getActivityRecord(); doReturn(1L).when(primaryActivity).getLastWindowCreateTime(); doReturn(2L).when(secondaryActivity).getLastWindowCreateTime(); + doReturn(mDisplayContent).when(primaryActivity).getDisplayContent(); + doReturn(secondaryWindow).when(mDisplayContent).findFocusedWindow(eq(secondaryActivity)); - startBackNavigation(); - verify(mWm).moveFocusToActivity(eq(secondaryActivity)); + final WindowState mostRecentUsedWindow = + mWm.getMostRecentUsedEmbeddedWindowForBack(primaryWindow); + assertThat(mostRecentUsedWindow).isEqualTo(secondaryWindow); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index 23a88a1774f4..b687042edfc3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,9 +21,19 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; +import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; +import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING; +import static com.android.server.wm.DesktopModeBoundsCalculator.calculateAspectRatio; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; @@ -59,6 +69,10 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) public class DesktopModeLaunchParamsModifierTests extends LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> { + private static final Rect LANDSCAPE_DISPLAY_BOUNDS = new Rect(0, 0, 2560, 1600); + private static final Rect PORTRAIT_DISPLAY_BOUNDS = new Rect(0, 0, 1600, 2560); + private static final float LETTERBOX_ASPECT_RATIO = 1.3f; + @Before public void setUp() throws Exception { mActivity = new ActivityBuilder(mAtm).build(); @@ -158,6 +172,7 @@ public class DesktopModeLaunchParamsModifierTests extends @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() { setupDesktopModeLaunchParamsModifier(); @@ -169,6 +184,209 @@ public class DesktopModeLaunchParamsModifierTests extends (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); final int desiredHeight = (int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultLandscapeBounds_landscapeDevice_resizable_undefinedOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true); + + final int desiredWidth = + (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultLandscapeBounds_landscapeDevice_resizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true); + + final int desiredWidth = + (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + public void testResizablePortraitBounds_landscapeDevice_resizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true); + + final int desiredWidth = + (int) ((LANDSCAPE_DISPLAY_BOUNDS.height() / LETTERBOX_ASPECT_RATIO) + 0.5f); + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false); + + final int desiredWidth = + (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false); + + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultPortraitBounds_portraitDevice_resizable_undefinedOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true); + + final int desiredWidth = + (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultPortraitBounds_portraitDevice_resizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true); + + final int desiredWidth = + (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + public void testResizableLandscapeBounds_portraitDevice_resizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true); + + final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width() + - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); + final int desiredHeight = (int) + ((PORTRAIT_DISPLAY_BOUNDS.width() / LETTERBOX_ASPECT_RATIO) + 0.5f); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false); + + final int desiredWidth = + (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false); + + final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width() + - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); + final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); assertEquals(desiredWidth, mResult.mBounds.width()); assertEquals(desiredHeight, mResult.mBounds.height()); @@ -192,6 +410,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_CenterToDisplay() { setupDesktopModeLaunchParamsModifier(); @@ -207,6 +426,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_LeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -222,6 +442,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_TopGravity() { setupDesktopModeLaunchParamsModifier(); @@ -237,6 +458,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_TopLeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -252,6 +474,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_RightGravity() { setupDesktopModeLaunchParamsModifier(); @@ -267,6 +490,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_BottomGravity() { setupDesktopModeLaunchParamsModifier(); @@ -282,6 +506,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_RightBottomGravity() { setupDesktopModeLaunchParamsModifier(); @@ -297,6 +522,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutFractionBounds() { setupDesktopModeLaunchParamsModifier(); @@ -312,6 +538,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -327,6 +554,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() { setupDesktopModeLaunchParamsModifier(); @@ -342,6 +570,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -359,6 +588,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() { setupDesktopModeLaunchParamsModifier(); @@ -374,6 +604,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() { setupDesktopModeLaunchParamsModifier(); @@ -389,6 +620,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() { setupDesktopModeLaunchParamsModifier(); @@ -422,6 +654,38 @@ public class DesktopModeLaunchParamsModifierTests extends assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode); } + private Task createTask(DisplayContent display, int orientation, Boolean isResizeable) { + final int resizeMode = isResizeable ? RESIZE_MODE_RESIZEABLE + : RESIZE_MODE_UNRESIZEABLE; + final Task task = new TaskBuilder(mSupervisor).setActivityType( + ACTIVITY_TYPE_STANDARD).setDisplay(display).build(); + task.setResizeMode(resizeMode); + mActivity = new ActivityBuilder(task.mAtmService) + .setTask(task) + .setScreenOrientation(orientation) + .setOnTop(true).build(); + + mActivity.onDisplayChanged(display); + mActivity.setOccludesParent(true); + mActivity.setVisible(true); + mActivity.setVisibleRequested(true); + mActivity.mDisplayContent.setIgnoreOrientationRequest(/* ignoreOrientationRequest */ true); + + return task; + } + + private TestDisplayContent createDisplayContent(int orientation, Rect displayBounds) { + final TestDisplayContent display = new TestDisplayContent + .Builder(mAtm, displayBounds.width(), displayBounds.height()) + .setPosition(DisplayContent.POSITION_TOP).build(); + display.setBounds(displayBounds); + display.getConfiguration().densityDpi = DENSITY_DEFAULT; + display.getConfiguration().orientation = ORIENTATION_LANDSCAPE; + display.getDefaultTaskDisplayArea().setWindowingMode(orientation); + + return display; + } + private void setupDesktopModeLaunchParamsModifier() { setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true, /*enforceDeviceRestrictions=*/ true); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index d318f0047c08..44c7057b2294 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -19,19 +19,12 @@ package com.android.server.wm; import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; -import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; -import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION; -import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2; -import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE; -import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; -import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; -import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -119,8 +112,6 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mController = new LetterboxUiController(mWm, mActivity); } - - @Test public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() { final InsetsSource taskbar = new InsetsSource(/*id=*/ 0, @@ -320,164 +311,6 @@ public class LetterboxUiControllerTest extends WindowTestsBase { return mainWindow; } - // shouldApplyUser...Override - @Test - public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, - /* value */ true); - - doReturn(false).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserFullscreenOverride()); - } - - @Test - public void testShouldApplyUserFullscreenOverride_falseFullscreenProperty_returnsFalse() - throws Exception { - prepareActivityThatShouldApplyUserFullscreenOverride(); - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, - /* value */ false); - - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserFullscreenOverride()); - } - - @Test - public void testShouldApplyUserFullscreenOverride_falseSettingsProperty_returnsFalse() - throws Exception { - prepareActivityThatShouldApplyUserFullscreenOverride(); - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false); - - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserFullscreenOverride()); - } - - @Test - public void testShouldApplyUserFullscreenOverride_returnsTrue() { - prepareActivityThatShouldApplyUserFullscreenOverride(); - - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserFullscreenOverride()); - } - - @Test - public void testShouldEnableUserAspectRatioSettings_falseProperty_returnsFalse() - throws Exception { - prepareActivityThatShouldApplyUserMinAspectRatioOverride(); - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false); - - mActivity = setUpActivityWithComponent(); - mController = new LetterboxUiController(mWm, mActivity); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldEnableUserAspectRatioSettings()); - } - - @Test - public void testShouldEnableUserAspectRatioSettings_trueProperty_returnsTrue() - throws Exception { - - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - prepareActivityThatShouldApplyUserMinAspectRatioOverride(); - - mController = new LetterboxUiController(mWm, mActivity); - - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldEnableUserAspectRatioSettings()); - } - - @Test - public void testShouldEnableUserAspectRatioSettings_noIgnoreOrientation_returnsFalse() - throws Exception { - prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ false); - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true); - - mController = new LetterboxUiController(mWm, mActivity); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldEnableUserAspectRatioSettings()); - } - - @Test - public void testShouldApplyUserMinAspectRatioOverride_falseProperty_returnsFalse() - throws Exception { - prepareActivityThatShouldApplyUserMinAspectRatioOverride(); - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false); - - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldEnableUserAspectRatioSettings()); - } - - @Test - public void testShouldApplyUserMinAspectRatioOverride_trueProperty_returnsFalse() - throws Exception { - doReturn(false).when(mAppCompatConfiguration).isUserAppAspectRatioSettingsEnabled(); - mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true); - - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserMinAspectRatioOverride()); - } - - @Test - public void testShouldApplyUserMinAspectRatioOverride_disabledIgnoreOrientationRequest() { - prepareActivityThatShouldApplyUserMinAspectRatioOverride(); - mDisplayContent.setIgnoreOrientationRequest(false); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserMinAspectRatioOverride()); - } - - @Test - public void testShouldApplyUserMinAspectRatioOverride_returnsTrue() { - prepareActivityThatShouldApplyUserMinAspectRatioOverride(); - - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserMinAspectRatioOverride()); - } - - @Test - public void testShouldApplyUserMinAspectRatioOverride_noIgnoreOrientation_returnsFalse() { - prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ false); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldApplyUserMinAspectRatioOverride()); - } - - private void prepareActivityForShouldApplyUserMinAspectRatioOverride( - boolean orientationRequest) { - spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); - doReturn(orientationRequest).when( - mAppCompatConfiguration).isUserAppAspectRatioSettingsEnabled(); - mDisplayContent.setIgnoreOrientationRequest(true); - doReturn(USER_MIN_ASPECT_RATIO_3_2) - .when(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) - .getUserMinAspectRatioOverrideCode(); - } - - private void prepareActivityThatShouldApplyUserMinAspectRatioOverride() { - prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ true); - } - - private void prepareActivityThatShouldApplyUserFullscreenOverride() { - spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); - doReturn(true).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); - mDisplayContent.setIgnoreOrientationRequest(true); - doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN) - .when(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) - .getUserMinAspectRatioOverrideCode(); - } - // shouldUseDisplayLandscapeNaturalOrientation @Test @@ -595,156 +428,6 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) - public void testshouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() { - mActivity = setUpActivityWithComponent(); - - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldOverrideMinAspectRatio()); - } - - @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) - public void testshouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldOverrideMinAspectRatio()); - } - - @Test - @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) - public void testshouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldOverrideMinAspectRatio()); - } - - @Test - @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) - public void testshouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() { - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldOverrideMinAspectRatio()); - } - - @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) - public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); - - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldOverrideMinAspectRatio()); - } - - @Test - @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) - public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldOverrideMinAspectRatio()); - } - - @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_overrideEnabled_returnsTrue() { - mActivity = setUpActivityWithComponent(); - doReturn(true).when(mActivity.mAppCompatController - .getAppCompatCameraOverrides()).isCameraActive(); - - assertTrue(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsTrue() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - doReturn(true).when(mActivity.mAppCompatController - .getAppCompatCameraOverrides()).isCameraActive(); - - assertTrue(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - doReturn(false).when(mActivity.mAppCompatController - .getAppCompatCameraOverrides()).isCameraActive(); - - assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test - @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideDisabled_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); - mActivity = setUpActivityWithComponent(); - doReturn(true).when(mActivity.mAppCompatController - .getAppCompatCameraOverrides()).isCameraActive(); - - assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test - @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_overrideDisabled_returnsFalse() { - mActivity = setUpActivityWithComponent(); - doReturn(true).when(mActivity.mAppCompatController - .getAppCompatCameraOverrides()).isCameraActive(); - - assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test - @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_propertyFalse_overrideEnabled_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); - mActivity = setUpActivityWithComponent(); - - assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test - @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void shouldOverrideMinAspectRatioForCamera_propertyFalse_noOverride_returnsFalse() - throws Exception { - mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); - - mActivity = setUpActivityWithComponent(); - - doReturn(true).when(mActivity.mAppCompatController - .getAppCompatCameraOverrides()).isCameraActive(); - - assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides() - .shouldOverrideMinAspectRatioForCamera()); - } - - @Test @EnableCompatChanges({FORCE_RESIZE_APP}) public void testshouldOverrideForceResizeApp_overrideEnabled_returnsTrue() { mController = new LetterboxUiController(mWm, mActivity); diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index 0bf27d11493b..f93ffb83178f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -747,6 +747,8 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { } } + @android.platform.test.annotations.RequiresFlagsDisabled( + com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testLaunchRemoteAnimationWithoutImeBehind() { diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java index e3f8e8c54b73..8abf3f86c01e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java @@ -36,8 +36,8 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import androidx.annotation.NonNull; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.wm.utils.CommonUtils; diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index a816aa9b7598..d5d284783978 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -203,6 +203,7 @@ public class SystemServicesTestRule implements TestRule { .mockStatic(LockGuard.class, mockStubOnly) .mockStatic(Watchdog.class, mockStubOnly) .spyStatic(DesktopModeHelper.class) + .spyStatic(DesktopModeBoundsCalculator.class) .strictness(Strictness.LENIENT) .startMocking(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java deleted file mode 100644 index d5356774ffdb..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.internal.policy.TaskResizingAlgorithm.MIN_ASPECT; -import static com.android.server.wm.WindowManagerService.dipToPixel; -import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP; -import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.graphics.Rect; -import android.platform.test.annotations.Presubmit; -import android.util.DisplayMetrics; -import android.util.Log; - -import androidx.test.filters.SmallTest; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests for the {@link TaskPositioner} class. - * - * Build/Install/Run: - * atest WmTests:TaskPositionerTests - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class TaskPositionerTests extends WindowTestsBase { - - private static final boolean DEBUGGING = false; - private static final String TAG = "TaskPositionerTest"; - - private static final int MOUSE_DELTA_X = 5; - private static final int MOUSE_DELTA_Y = 5; - - private int mMinVisibleWidth; - private int mMinVisibleHeight; - private TaskPositioner mPositioner; - - @Before - public void setUp() { - TaskPositioner.setFactory(null); - - final DisplayMetrics dm = mDisplayContent.getDisplayMetrics(); - - // This should be the same calculation as the TaskPositioner uses. - mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, dm); - mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, dm); - removeGlobalMinSizeRestriction(); - - final ActivityRecord activity = new ActivityBuilder(mAtm) - .setCreateTask(true) - .build(); - final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "window"); - mPositioner = new TaskPositioner(mWm); - mPositioner.register(mDisplayContent, win); - - win.getRootTask().setWindowingMode(WINDOWING_MODE_FREEFORM); - } - - @After - public void tearDown() { - TaskPositioner.setFactory(null); - } - - @Test - public void testOverrideFactory() { - final boolean[] created = new boolean[1]; - created[0] = false; - TaskPositioner.setFactory(new TaskPositioner.Factory() { - @Override - public TaskPositioner create(WindowManagerService service) { - created[0] = true; - return null; - } - }); - - assertNull(TaskPositioner.create(mWm)); - assertTrue(created[0]); - } - - /** This tests that the window can move in all directions. */ - @Test - public void testMoveWindow() { - final Rect displayBounds = mDisplayContent.getBounds(); - final int windowSize = Math.min(displayBounds.width(), displayBounds.height()) / 2; - final int left = displayBounds.centerX() - windowSize / 2; - final int top = displayBounds.centerY() - windowSize / 2; - final Rect r = new Rect(left, top, left + windowSize, top + windowSize); - mPositioner.mTask.setBounds(r); - mPositioner.startDrag(false /* resizing */, false /* preserveOrientation */, left, top); - - // Move upper left. - mPositioner.notifyMoveLocked(left - MOUSE_DELTA_X, top - MOUSE_DELTA_Y); - r.offset(-MOUSE_DELTA_X, -MOUSE_DELTA_Y); - assertBoundsEquals(r, mPositioner.getWindowDragBounds()); - - // Move bottom right. - mPositioner.notifyMoveLocked(left, top); - r.offset(MOUSE_DELTA_X, MOUSE_DELTA_Y); - assertBoundsEquals(r, mPositioner.getWindowDragBounds()); - } - - /** - * This tests that free resizing will allow to change the orientation as well - * as does some basic tests (e.g. dragging in Y only will keep X stable). - */ - @Test - public void testBasicFreeWindowResizing() { - final Rect r = new Rect(100, 220, 700, 520); - final int midY = (r.top + r.bottom) / 2; - mPositioner.mTask.setBounds(r, true); - - // Start a drag resize starting upper left. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y); - assertBoundsEquals(r, mPositioner.getWindowDragBounds()); - - // Drag to a good landscape size. - mPositioner.resizeDrag(0.0f, 0.0f); - assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a good portrait size. - mPositioner.resizeDrag(400.0f, 0.0f); - assertBoundsEquals(new Rect(400 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a too small size for the width. - mPositioner.resizeDrag(2000.0f, r.top); - assertBoundsEquals( - new Rect(r.right - mMinVisibleWidth, r.top + MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a too small size for the height. - mPositioner.resizeDrag(r.left, 2000.0f); - assertBoundsEquals( - new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Start a drag resize left and see that only the left coord changes.. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.left - MOUSE_DELTA_X, midY); - - // Drag to the left. - mPositioner.resizeDrag(0.0f, midY); - assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the right. - mPositioner.resizeDrag(200.0f, midY); - assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the top - mPositioner.resizeDrag(r.left, 0.0f); - assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the bottom - mPositioner.resizeDrag(r.left, 1000.0f); - assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - } - - /** - * This tests that by dragging any edge, the fixed / opposite edge(s) remains anchored. - */ - @Test - public void testFreeWindowResizingTestAllEdges() { - final Rect r = new Rect(100, 220, 700, 520); - final int midX = (r.left + r.right) / 2; - final int midY = (r.top + r.bottom) / 2; - mPositioner.mTask.setBounds(r, true); - - // Drag upper left. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y); - mPositioner.resizeDrag(0.0f, 0.0f); - assertNotEquals(r.left, mPositioner.getWindowDragBounds().left); - assertEquals(r.right, mPositioner.getWindowDragBounds().right); - assertNotEquals(r.top, mPositioner.getWindowDragBounds().top); - assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag upper. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, midX, - r.top - MOUSE_DELTA_Y); - mPositioner.resizeDrag(0.0f, 0.0f); - assertEquals(r.left, mPositioner.getWindowDragBounds().left); - assertEquals(r.right, mPositioner.getWindowDragBounds().right); - assertNotEquals(r.top, mPositioner.getWindowDragBounds().top); - assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag upper right. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.right + MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y); - mPositioner.resizeDrag(r.right + 100, 0.0f); - assertEquals(r.left, mPositioner.getWindowDragBounds().left); - assertNotEquals(r.right, mPositioner.getWindowDragBounds().right); - assertNotEquals(r.top, mPositioner.getWindowDragBounds().top); - assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag right. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.right + MOUSE_DELTA_X, midY); - mPositioner.resizeDrag(r.right + 100, 0.0f); - assertEquals(r.left, mPositioner.getWindowDragBounds().left); - assertNotEquals(r.right, mPositioner.getWindowDragBounds().right); - assertEquals(r.top, mPositioner.getWindowDragBounds().top); - assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag bottom right. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.right + MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y); - mPositioner.resizeDrag(r.right + 100, r.bottom + 100); - assertEquals(r.left, mPositioner.getWindowDragBounds().left); - assertNotEquals(r.right, mPositioner.getWindowDragBounds().right); - assertEquals(r.top, mPositioner.getWindowDragBounds().top); - assertNotEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag bottom. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, midX, - r.bottom + MOUSE_DELTA_Y); - mPositioner.resizeDrag(r.right + 100, r.bottom + 100); - assertEquals(r.left, mPositioner.getWindowDragBounds().left); - assertEquals(r.right, mPositioner.getWindowDragBounds().right); - assertEquals(r.top, mPositioner.getWindowDragBounds().top); - assertNotEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag bottom left. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.left - MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y); - mPositioner.resizeDrag(0.0f, r.bottom + 100); - assertNotEquals(r.left, mPositioner.getWindowDragBounds().left); - assertEquals(r.right, mPositioner.getWindowDragBounds().right); - assertEquals(r.top, mPositioner.getWindowDragBounds().top); - assertNotEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - - // Drag left. - mPositioner.startDrag(true /* resizing */, false /* preserveOrientation */, - r.left - MOUSE_DELTA_X, midY); - mPositioner.resizeDrag(0.0f, r.bottom + 100); - assertNotEquals(r.left, mPositioner.getWindowDragBounds().left); - assertEquals(r.right, mPositioner.getWindowDragBounds().right); - assertEquals(r.top, mPositioner.getWindowDragBounds().top); - assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom); - } - - /** - * This tests that a constrained landscape window will keep the aspect and do the - * right things upon resizing when dragged from the top left corner. - */ - @Test - public void testLandscapePreservedWindowResizingDragTopLeft() { - final Rect r = new Rect(100, 220, 700, 520); - mPositioner.mTask.setBounds(r, true); - - mPositioner.startDrag(true /* resizing */, true /* preserveOrientation */, - r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y); - assertBoundsEquals(r, mPositioner.getWindowDragBounds()); - - // Drag to a good landscape size. - mPositioner.resizeDrag(0.0f, 0.0f); - assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a good portrait size. - mPositioner.resizeDrag(400.0f, 0.0f); - int width = Math.round((float) (r.bottom - MOUSE_DELTA_Y) * MIN_ASPECT); - assertBoundsEquals(new Rect(r.right - width, MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a too small size for the width. - mPositioner.resizeDrag(2000.0f, r.top); - final int w = mMinVisibleWidth; - final int h = Math.round(w / MIN_ASPECT); - assertBoundsEquals(new Rect(r.right - w, r.bottom - h, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a too small size for the height. - mPositioner.resizeDrag(r.left, 2000.0f); - assertBoundsEquals( - new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - } - - /** - * This tests that a constrained landscape window will keep the aspect and do the - * right things upon resizing when dragged from the left corner. - */ - @Test - public void testLandscapePreservedWindowResizingDragLeft() { - final Rect r = new Rect(100, 220, 700, 520); - final int midY = (r.top + r.bottom) / 2; - mPositioner.mTask.setBounds(r, true); - - mPositioner.startDrag(true /* resizing */, true /* preserveOrientation */, - r.left - MOUSE_DELTA_X, midY); - - // Drag to the left. - mPositioner.resizeDrag(0.0f, midY); - assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the right. - mPositioner.resizeDrag(200.0f, midY); - assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag all the way to the right and see the height also shrinking. - mPositioner.resizeDrag(2000.0f, midY); - final int w = mMinVisibleWidth; - final int h = Math.round((float) w / MIN_ASPECT); - assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h), - mPositioner.getWindowDragBounds()); - - // Drag to the top. - mPositioner.resizeDrag(r.left, 0.0f); - assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the bottom. - mPositioner.resizeDrag(r.left, 1000.0f); - assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - } - - /** - * This tests that a constrained landscape window will keep the aspect and do the - * right things upon resizing when dragged from the top corner. - */ - @Test - public void testLandscapePreservedWindowResizingDragTop() { - final Rect r = new Rect(100, 220, 700, 520); - final int midX = (r.left + r.right) / 2; - mPositioner.mTask.setBounds(r, true); - - mPositioner.startDrag(true /*resizing*/, true /*preserveOrientation*/, midX, - r.top - MOUSE_DELTA_Y); - - // Drag to the left (no change). - mPositioner.resizeDrag(0.0f, r.top); - assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the right (no change). - mPositioner.resizeDrag(2000.0f, r.top); - assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the top. - mPositioner.resizeDrag(300.0f, 0.0f); - int h = r.bottom - MOUSE_DELTA_Y; - int w = Math.max(r.right - r.left, Math.round(h * MIN_ASPECT)); - assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the bottom. - mPositioner.resizeDrag(r.left, 1000.0f); - h = mMinVisibleHeight; - assertBoundsEquals(new Rect(r.left, r.bottom - h, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - } - - /** - * This tests that a constrained portrait window will keep the aspect and do the - * right things upon resizing when dragged from the top left corner. - */ - @Test - public void testPortraitPreservedWindowResizingDragTopLeft() { - final Rect r = new Rect(330, 100, 630, 600); - mPositioner.mTask.setBounds(r, true); - - mPositioner.startDrag(true /*resizing*/, true /*preserveOrientation*/, - r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y); - assertBoundsEquals(r, mPositioner.getWindowDragBounds()); - - // Drag to a good landscape size. - mPositioner.resizeDrag(0.0f, 0.0f); - int height = Math.round((float) (r.right - MOUSE_DELTA_X) * MIN_ASPECT); - assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.bottom - height, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a good portrait size. - mPositioner.resizeDrag(400.0f, 0.0f); - assertBoundsEquals(new Rect(400 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to a too small size for the height and the the width shrinking. - mPositioner.resizeDrag(r.left + MOUSE_DELTA_X, 2000.0f); - final int w = Math.max(mMinVisibleWidth, Math.round(mMinVisibleHeight / MIN_ASPECT)); - final int h = Math.max(mMinVisibleHeight, Math.round(w * MIN_ASPECT)); - assertBoundsEquals( - new Rect(r.right - w, r.bottom - h, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - } - - /** - * This tests that a constrained portrait window will keep the aspect and do the - * right things upon resizing when dragged from the left corner. - */ - @Test - public void testPortraitPreservedWindowResizingDragLeft() { - final Rect r = new Rect(330, 100, 630, 600); - final int midY = (r.top + r.bottom) / 2; - mPositioner.mTask.setBounds(r, true); - - mPositioner.startDrag(true /* resizing */, true /* preserveOrientation */, - r.left - MOUSE_DELTA_X, midY); - - // Drag to the left. - mPositioner.resizeDrag(0.0f, midY); - int w = r.right - MOUSE_DELTA_X; - int h = Math.round(w * MIN_ASPECT); - assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.top + h), - mPositioner.getWindowDragBounds()); - - // Drag to the right. - mPositioner.resizeDrag(450.0f, midY); - assertBoundsEquals(new Rect(450 + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag all the way to the right. - mPositioner.resizeDrag(2000.0f, midY); - w = mMinVisibleWidth; - h = Math.max(Math.round((float) w * MIN_ASPECT), r.height()); - assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h), - mPositioner.getWindowDragBounds()); - - // Drag to the top. - mPositioner.resizeDrag(r.left, 0.0f); - assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the bottom. - mPositioner.resizeDrag(r.left, 1000.0f); - assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - } - - /** - * This tests that a constrained portrait window will keep the aspect and do the - * right things upon resizing when dragged from the top corner. - */ - @Test - public void testPortraitPreservedWindowResizingDragTop() { - final Rect r = new Rect(330, 100, 630, 600); - final int midX = (r.left + r.right) / 2; - mPositioner.mTask.setBounds(r, true); - - mPositioner.startDrag(true /* resizing */, true /* preserveOrientation */, midX, - r.top - MOUSE_DELTA_Y); - - // Drag to the left (no change). - mPositioner.resizeDrag(0.0f, r.top); - assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the right (no change). - mPositioner.resizeDrag(2000.0f, r.top); - assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the top. - mPositioner.resizeDrag(300.0f, 0.0f); - int h = r.bottom - MOUSE_DELTA_Y; - int w = Math.min(r.width(), Math.round(h / MIN_ASPECT)); - assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom), - mPositioner.getWindowDragBounds()); - - // Drag to the bottom. - mPositioner.resizeDrag(r.left, 1000.0f); - h = Math.max(mMinVisibleHeight, Math.round(mMinVisibleWidth * MIN_ASPECT)); - w = Math.round(h / MIN_ASPECT); - assertBoundsEquals(new Rect(r.left, r.bottom - h, r.left + w, r.bottom), - mPositioner.getWindowDragBounds()); - } - - private static void assertBoundsEquals(Rect expected, Rect actual) { - if (DEBUGGING) { - if (!expected.equals(actual)) { - Log.e(TAG, "rect(" + actual.toString() + ") != isRect(" + actual.toString() - + ") " + Log.getStackTraceString(new Throwable())); - } - } - assertEquals(expected, actual); - } - - @Test - public void testFinishingMovingWhenBinderDied() { - spyOn(mWm.mTaskPositioningController); - - mPositioner.startDrag(false, false, 0 /* startX */, 0 /* startY */); - verify(mWm.mTaskPositioningController, never()).finishTaskPositioning(); - mPositioner.binderDied(); - verify(mWm.mTaskPositioningController).finishTaskPositioning(); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java deleted file mode 100644 index bfc13d3d2ef2..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/TaskPositioningControllerTests.java +++ /dev/null @@ -1,152 +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.server.wm; - -import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; -import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import android.platform.test.annotations.Presubmit; -import android.view.InputChannel; - -import androidx.test.filters.FlakyTest; -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests for the {@link TaskPositioningController} class. - * - * Build/Install/Run: - * atest WmTests:TaskPositioningControllerTests - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class TaskPositioningControllerTests extends WindowTestsBase { - private static final int TIMEOUT_MS = 1000; - - private TaskPositioningController mTarget; - private WindowState mWindow; - - @Before - public void setUp() throws Exception { - assertNotNull(mWm.mTaskPositioningController); - mTarget = mWm.mTaskPositioningController; - - when(mWm.mInputManager.transferTouchGesture(any(), any())).thenReturn(true); - - mWindow = createWindow(null, TYPE_BASE_APPLICATION, "window"); - mWindow.getTask().setResizeMode(RESIZE_MODE_RESIZEABLE); - mWindow.mInputChannel = new InputChannel(); - mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow); - doReturn(mock(InputMonitor.class)).when(mDisplayContent).getInputMonitor(); - } - - @FlakyTest(bugId = 291067614) - @Test - public void testStartAndFinishPositioning() { - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - - assertTrue(mTarget.startMovingTask(mWindow.mClient, 0, 0)); - - assertTrue(mTarget.isPositioningLocked()); - assertNotNull(mTarget.getDragWindowHandleLocked()); - - mTarget.finishTaskPositioning(); - // Wait until the looper processes finishTaskPositioning. - assertTrue(waitHandlerIdle(mWm.mAnimationHandler, TIMEOUT_MS)); - - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - } - - @Test - public void testFinishPositioningWhenAppRequested() { - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - - assertTrue(mTarget.startMovingTask(mWindow.mClient, 0, 0)); - - assertTrue(mTarget.isPositioningLocked()); - assertNotNull(mTarget.getDragWindowHandleLocked()); - - mTarget.finishTaskPositioning(mWindow.mClient); - // Wait until the looper processes finishTaskPositioning. - assertTrue(waitHandlerIdle(mWm.mAnimationHandler, TIMEOUT_MS)); - - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - } - - @Test - public void testHandleTapOutsideTask() { - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - - final DisplayContent content = mock(DisplayContent.class); - doReturn(mWindow.getTask()).when(content).findTaskForResizePoint(anyInt(), anyInt()); - assertNotNull(mWindow.getTask().getTopVisibleAppMainWindow()); - - mTarget.handleTapOutsideTask(content, 0, 0); - // Wait until the looper processes handleTapOutsideTask. - assertTrue(waitHandlerIdle(mWm.mH, TIMEOUT_MS)); - - assertTrue(mTarget.isPositioningLocked()); - assertNotNull(mTarget.getDragWindowHandleLocked()); - - mTarget.finishTaskPositioning(); - // Wait until the looper processes finishTaskPositioning. - assertTrue(waitHandlerIdle(mWm.mAnimationHandler, TIMEOUT_MS)); - - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - } - - @Test - public void testHandleTapOutsideNonResizableTask() { - assertFalse(mTarget.isPositioningLocked()); - assertNull(mTarget.getDragWindowHandleLocked()); - - final DisplayContent content = mock(DisplayContent.class); - doReturn(mWindow.getTask()).when(content).findTaskForResizePoint(anyInt(), anyInt()); - assertNotNull(mWindow.getTask().getTopVisibleAppMainWindow()); - - mWindow.getTask().setResizeMode(RESIZE_MODE_UNRESIZEABLE); - - mTarget.handleTapOutsideTask(content, 0, 0); - // Wait until the looper processes handleTapOutsideTask. - assertTrue(waitHandlerIdle(mWm.mH, TIMEOUT_MS)); - - assertFalse(mTarget.isPositioningLocked()); - } - -} diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 7d01b79fe866..720457e24370 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1573,7 +1573,8 @@ public class TransitionTests extends WindowTestsBase { enteringAnimReports.clear(); doCallRealMethod().when(mWm.mRoot).ensureActivitiesVisible(any(), anyBoolean()); final boolean[] wasInFinishingTransition = { false }; - controller.registerLegacyListener(new WindowManagerInternal.AppTransitionListener() { + controller.registerLegacyListener(new WindowManagerInternal.AppTransitionListener( + mDisplayContent.mDisplayId) { @Override public void onAppTransitionFinishedLocked(IBinder token) { final ActivityRecord r = ActivityRecord.forToken(token); @@ -1582,6 +1583,14 @@ public class TransitionTests extends WindowTestsBase { } } }); + final boolean[] calledListenerOnOtherDisplay = { false }; + controller.registerLegacyListener(new WindowManagerInternal.AppTransitionListener( + mDisplayContent.mDisplayId + 1234) { + @Override + public void onAppTransitionFinishedLocked(IBinder token) { + calledListenerOnOtherDisplay[0] = true; + } + }); assertTrue(activity1.isVisible()); doReturn(false).when(task1).isTranslucent(null); doReturn(false).when(task1).isTranslucentForTransition(); @@ -1592,6 +1601,7 @@ public class TransitionTests extends WindowTestsBase { controller.finishTransition(closeTransition); assertTrue(wasInFinishingTransition[0]); + assertFalse(calledListenerOnOtherDisplay[0]); assertNull(controller.mFinishingTransition); assertTrue(activity2.isVisible()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java index 593e983f3d23..22def515a98e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.eq; import android.platform.test.annotations.Presubmit; @@ -60,4 +61,24 @@ public class WindowContainerTraversalTests extends WindowTestsBase { verify(c).accept(eq(mDockedDividerWindow)); verify(c).accept(eq(mImeWindow)); } + + @android.platform.test.annotations.RequiresFlagsEnabled( + com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) + @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) + @Test + public void testTraverseImeRegardlessOfImeTarget() { + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + mAppWindow.mHasSurface = false; + mAppWindow.mActivityRecord.setVisibleRequested(false); + mAppWindow.mActivityRecord.setVisible(false); + + final boolean[] foundIme = { false }; + mDisplayContent.forAllWindows(w -> { + if (w == mImeWindow) { + foundIme[0] = true; + } + }, true /* traverseTopToBottom */); + assertTrue("IME must be found", foundIme[0]); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java index e6648dad4bbe..0cb22ad47355 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED; import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; @@ -86,6 +87,7 @@ public class WindowProcessControllerTests extends WindowTestsBase { ApplicationInfo info = mock(ApplicationInfo.class); info.packageName = "test.package.name"; + doReturn(true).when(info).isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED); mWpc = new WindowProcessController( mAtm, info, null, 0, -1, null, mMockListener); mWpc.setThread(mock(IApplicationThread.class)); diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java index e5f2f89ccead..eda78cb40c5d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java @@ -63,9 +63,6 @@ public class DesktopModeFlagsUtilTest extends WindowTestsBase { resetCache(); } - private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = - "sys.wmshell.desktopmode.dev_toggle_override"; - @Test @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @@ -190,110 +187,6 @@ public class DesktopModeFlagsUtilTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() { - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); - setOverride(OVERRIDE_ON.getSetting()); - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); - // Store System Property if not present - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); - } - - @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() { - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); - setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); - // Store System Property if not present - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf( - DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() { - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); - setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); - // Store System Property if not present - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf( - DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); - } - - @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc"); - setOverride(OVERRIDE_OFF.getSetting()); - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); - // Store System Property if currently invalid - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); - } - - @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2"); - setOverride(OVERRIDE_OFF.getSetting()); - - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); - // Store System Property if currently invalid - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); - } - - @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf( - OVERRIDE_OFF.getSetting())); - setOverride(OVERRIDE_ON.getSetting()); - - // Have a consistent override until reboot - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting())); - setOverride(OVERRIDE_OFF.getSetting()); - - // Have a consistent override until reboot - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); - } - - @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() { - System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, - String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); - setOverride(OVERRIDE_OFF.getSetting()); - - // Have a consistent override until reboot - assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); - assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) - .isEqualTo(String.valueOf( - DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); - } - - @Test @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY}) public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() { @@ -452,8 +345,5 @@ public class DesktopModeFlagsUtilTest extends WindowTestsBase { "sCachedToggleOverride"); cachedToggleOverride.setAccessible(true); cachedToggleOverride.set(null, null); - - // Clear override cache stored in System property - System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); } } diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 175a09db54e3..14044135eca7 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -16,6 +16,8 @@ package com.android.server.usb; +import com.android.internal.annotations.Keep; + import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; import static android.hardware.usb.UsbPortStatus.DATA_ROLE_HOST; import static android.hardware.usb.UsbPortStatus.MODE_AUDIO_ACCESSORY; @@ -82,6 +84,7 @@ import android.util.Pair; import android.util.Slog; import android.text.TextUtils; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -147,6 +150,8 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser "DEVPATH=/devices/virtual/android_usb/android0"; private static final String ACCESSORY_START_MATCH = "DEVPATH=/devices/virtual/misc/usb_accessory"; + private static final String UDC_SUBSYS_MATCH = + "SUBSYSTEM=udc"; private static final String FUNCTIONS_PATH = "/sys/class/android_usb/android0/functions"; private static final String STATE_PATH = @@ -226,6 +231,9 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser private static UsbGadgetHal mUsbGadgetHal; + private final boolean mEnableUdcSysfsUsbStateUpdate; + private String mUdcName = ""; + /** * Counter for tracking UsbOperation operations. */ @@ -260,12 +268,9 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser if (DEBUG) Slog.d(TAG, "sEventLogger == null"); } - String state = event.get("USB_STATE"); String accessory = event.get("ACCESSORY"); - if (state != null) { - mHandler.updateState(state); - } else if ("GETPROTOCOL".equals(accessory)) { + if ("GETPROTOCOL".equals(accessory)) { if (DEBUG) Slog.d(TAG, "got accessory get protocol"); mHandler.setAccessoryUEventTime(SystemClock.elapsedRealtime()); resetAccessoryHandshakeTimeoutHandler(); @@ -279,6 +284,24 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser mHandler.setStartAccessoryTrue(); startAccessoryMode(); } + + if (mEnableUdcSysfsUsbStateUpdate) { + if (!mUdcName.isEmpty() + && "udc".equals(event.get("SUBSYSTEM")) + && event.get("DEVPATH").contains(mUdcName)) { + String action = event.get("ACTION"); + if ("add".equals(action)) { + nativeStartGadgetMonitor(mUdcName); + } else if ("remove".equals(action)) { + nativeStopGadgetMonitor(); + } + } + } else { + String state = event.get("USB_STATE"); + if (state != null) { + mHandler.updateState(state); + } + } } } @@ -406,9 +429,28 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser // Watch for USB configuration changes mUEventObserver = new UsbUEventObserver(); - mUEventObserver.startObserving(USB_STATE_MATCH); mUEventObserver.startObserving(ACCESSORY_START_MATCH); + mEnableUdcSysfsUsbStateUpdate = + android.hardware.usb.flags.Flags.enableUdcSysfsUsbStateUpdate() + && context.getResources().getBoolean(R.bool.config_enableUdcSysfsUsbStateUpdate); + + if (mEnableUdcSysfsUsbStateUpdate) { + mUEventObserver.startObserving(UDC_SUBSYS_MATCH); + new Thread("GetUsbControllerSysprop") { + public void run() { + String udcName; + // blocking wait until usb controller sysprop is available + udcName = nativeWaitAndGetProperty(USB_CONTROLLER_NAME_PROPERTY); + nativeStartGadgetMonitor(udcName); + mUdcName = udcName; + Slog.v(TAG, "USB controller name " + udcName); + } + }.start(); + } else { + mUEventObserver.startObserving(USB_STATE_MATCH); + } + sEventLogger = new EventLogger(DUMPSYS_LOG_BUFFER, "UsbDeviceManager activity"); } @@ -2609,11 +2651,27 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser dump.end(token); } + /** + * Update usb state (Called by native code). + */ + @Keep + private void updateGadgetState(String state) { + Slog.d(TAG, "Usb state update " + state); + + mHandler.updateState(state); + } + private native String[] nativeGetAccessoryStrings(); private native ParcelFileDescriptor nativeOpenAccessory(); + private native String nativeWaitAndGetProperty(String propName); + private native FileDescriptor nativeOpenControl(String usbFunction); private native boolean nativeIsStartRequested(); + + private native boolean nativeStartGadgetMonitor(String udcName); + + private native void nativeStopGadgetMonitor(); } diff --git a/telephony/OWNERS b/telephony/OWNERS index 7607c64150d8..92af034217a9 100644 --- a/telephony/OWNERS +++ b/telephony/OWNERS @@ -15,4 +15,4 @@ per-file CarrierConfigManager.java=set noparent per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com #Domain Selection is jointly owned, add additional owners for domain selection specific files -per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com +per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,jaesikkong@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl index 2dc8ffba6e96..460de8c8113d 100644 --- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl +++ b/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl @@ -25,4 +25,7 @@ parcelable ProvisionSubscriberId { /** carrier id */ int mCarrierId; + + /** apn */ + String mNiddApn; } diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java index 08430f2f2744..4143f595f9a0 100644 --- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java +++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java @@ -159,7 +159,7 @@ public class BatteryUsageStatsPerfTest { private static BatteryUsageStats buildBatteryUsageStats() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, false, false, 0) .setBatteryCapacity(4000) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index b6672a0e2f4b..fad94d45c85d 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -698,7 +698,7 @@ public class PerfettoProtoLogImplTest { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, - "My test null string: %s", null); + "My test null string: %s", (Object) null); } finally { traceMonitor.stop(mWriter); } diff --git a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java index be9fb1b309f6..9a062e3b2f80 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java @@ -67,7 +67,7 @@ public class ProtologDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {}); + final ProtoLogDataSource ds = new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( @@ -154,7 +154,7 @@ public class ProtologDataSourceTest { private ProtoLogDataSource.TlsState createTlsState( DataSourceConfigOuterClass.DataSourceConfig config) { final ProtoLogDataSource ds = - Mockito.spy(new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {})); + Mockito.spy(new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {})); ProtoInputStream configStream = new ProtoInputStream(config.toByteArray()); final ProtoLogDataSource.Instance dsInstance = Mockito.spy( |