diff options
678 files changed, 20869 insertions, 7142 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 2ae72ef4e81c..f5bf437a738d 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -13,70 +13,143 @@ // limitations under the License. aconfig_srcjars = [ - ":android.app.usage.flags-aconfig-java{.generated_srcjars}", + // !!! KEEP THIS LIST ALPHABETICAL !!! + ":aconfig_mediacodec_flags_java_lib{.generated_srcjars}", + ":android.adaptiveauth.flags-aconfig-java{.generated_srcjars}", + ":android.app.flags-aconfig-java{.generated_srcjars}", ":android.app.smartspace.flags-aconfig-java{.generated_srcjars}", + ":android.app.usage.flags-aconfig-java{.generated_srcjars}", + ":android.appwidget.flags-aconfig-java{.generated_srcjars}", + ":android.chre.flags-aconfig-java{.generated_srcjars}", ":android.companion.flags-aconfig-java{.generated_srcjars}", + ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}", + ":android.content.flags-aconfig-java{.generated_srcjars}", ":android.content.pm.flags-aconfig-java{.generated_srcjars}", ":android.content.res.flags-aconfig-java{.generated_srcjars}", + ":android.credentials.flags-aconfig-java{.generated_srcjars}", + ":android.database.sqlite-aconfig-java{.generated_srcjars}", + ":android.hardware.biometrics.flags-aconfig-java{.generated_srcjars}", ":android.hardware.flags-aconfig-java{.generated_srcjars}", ":android.hardware.radio.flags-aconfig-java{.generated_srcjars}", + ":android.hardware.usb.flags-aconfig-java{.generated_srcjars}", ":android.location.flags-aconfig-java{.generated_srcjars}", + ":android.media.tv.flags-aconfig-java{.generated_srcjars}", + ":android.multiuser.flags-aconfig-java{.generated_srcjars}", ":android.net.vcn.flags-aconfig-java{.generated_srcjars}", ":android.nfc.flags-aconfig-java{.generated_srcjars}", ":android.os.flags-aconfig-java{.generated_srcjars}", ":android.os.vibrator.flags-aconfig-java{.generated_srcjars}", + ":android.permission.flags-aconfig-java{.generated_srcjars}", + ":android.provider.flags-aconfig-java{.generated_srcjars}", ":android.security.flags-aconfig-java{.generated_srcjars}", ":android.server.app.flags-aconfig-java{.generated_srcjars}", + ":android.service.autofill.flags-aconfig-java{.generated_srcjars}", ":android.service.chooser.flags-aconfig-java{.generated_srcjars}", + ":android.service.controls.flags-aconfig-java{.generated_srcjars}", ":android.service.dreams.flags-aconfig-java{.generated_srcjars}", ":android.service.notification.flags-aconfig-java{.generated_srcjars}", - ":android.view.flags-aconfig-java{.generated_srcjars}", + ":android.service.voice.flags-aconfig-java{.generated_srcjars}", + ":android.speech.flags-aconfig-java{.generated_srcjars}", + ":android.tracing.flags-aconfig-java{.generated_srcjars}", ":android.view.accessibility.flags-aconfig-java{.generated_srcjars}", + ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}", + ":android.view.contentprotection.flags-aconfig-java{.generated_srcjars}", + ":android.view.flags-aconfig-java{.generated_srcjars}", + ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}", + ":android.webkit.flags-aconfig-java{.generated_srcjars}", + ":android.widget.flags-aconfig-java{.generated_srcjars}", ":audio-framework-aconfig", ":camera_platform_flags_core_java_lib{.generated_srcjars}", - ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", - ":android.hardware.biometrics.flags-aconfig-java{.generated_srcjars}", ":com.android.hardware.input-aconfig-java{.generated_srcjars}", ":com.android.input.flags-aconfig-java{.generated_srcjars}", - ":com.android.text.flags-aconfig-java{.generated_srcjars}", - ":framework-jobscheduler-job.flags-aconfig-java{.generated_srcjars}", - ":telecom_flags_core_java_lib{.generated_srcjars}", - ":telephony_flags_core_java_lib{.generated_srcjars}", - ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}", - ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}", - ":android.widget.flags-aconfig-java{.generated_srcjars}", - ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", - ":sdk_sandbox_flags_lib{.generated_srcjars}", - ":android.permission.flags-aconfig-java{.generated_srcjars}", - ":android.database.sqlite-aconfig-java{.generated_srcjars}", - ":hwui_flags_java_lib{.generated_srcjars}", - ":framework_graphics_flags_java_lib{.generated_srcjars}", - ":display_flags_lib{.generated_srcjars}", ":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}", - ":android.multiuser.flags-aconfig-java{.generated_srcjars}", - ":android.app.flags-aconfig-java{.generated_srcjars}", - ":android.credentials.flags-aconfig-java{.generated_srcjars}", - ":android.view.contentprotection.flags-aconfig-java{.generated_srcjars}", - ":com.android.server.flags.services-aconfig-java{.generated_srcjars}", - ":android.service.controls.flags-aconfig-java{.generated_srcjars}", - ":android.service.voice.flags-aconfig-java{.generated_srcjars}", - ":android.media.tv.flags-aconfig-java{.generated_srcjars}", - ":android.service.autofill.flags-aconfig-java{.generated_srcjars}", + ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", ":com.android.net.flags-aconfig-java{.generated_srcjars}", + ":com.android.server.flags.services-aconfig-java{.generated_srcjars}", + ":com.android.text.flags-aconfig-java{.generated_srcjars}", + ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", ":device_policy_aconfig_flags_lib{.generated_srcjars}", - ":surfaceflinger_flags_java_lib{.generated_srcjars}", - ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}", - ":android.hardware.usb.flags-aconfig-java{.generated_srcjars}", - ":android.tracing.flags-aconfig-java{.generated_srcjars}", - ":android.appwidget.flags-aconfig-java{.generated_srcjars}", - ":android.webkit.flags-aconfig-java{.generated_srcjars}", - ":android.provider.flags-aconfig-java{.generated_srcjars}", - ":android.chre.flags-aconfig-java{.generated_srcjars}", - ":android.speech.flags-aconfig-java{.generated_srcjars}", + ":display_flags_lib{.generated_srcjars}", + ":framework-jobscheduler-job.flags-aconfig-java{.generated_srcjars}", + ":framework_graphics_flags_java_lib{.generated_srcjars}", + ":hwui_flags_java_lib{.generated_srcjars}", ":power_flags_lib{.generated_srcjars}", - ":android.content.flags-aconfig-java{.generated_srcjars}", + ":sdk_sandbox_flags_lib{.generated_srcjars}", + ":surfaceflinger_flags_java_lib{.generated_srcjars}", + ":telecom_flags_core_java_lib{.generated_srcjars}", + ":telephony_flags_core_java_lib{.generated_srcjars}", + // !!! KEEP THIS LIST ALPHABETICAL !!! ] +stubs_defaults { + name: "framework-minus-apex-aconfig-declarations", + aconfig_declarations: [ + "android.app.flags-aconfig", + "android.app.smartspace.flags-aconfig", + "android.app.usage.flags-aconfig", + "android.appwidget.flags-aconfig", + "android.companion.flags-aconfig", + "android.companion.virtual.flags-aconfig", + "android.content.pm.flags-aconfig", + "android.content.res.flags-aconfig", + "android.credentials.flags-aconfig", + "android.database.sqlite-aconfig", + "android.hardware.biometrics.flags-aconfig", + "android.hardware.flags-aconfig", + "android.hardware.radio.flags-aconfig", + "android.hardware.usb.flags-aconfig", + "android.location.flags-aconfig", + "android.media.audio-aconfig", + "android.media.audiopolicy-aconfig", + "android.media.midi-aconfig", + "android.media.tv.flags-aconfig", + "android.multiuser.flags-aconfig", + "android.net.vcn.flags-aconfig", + "android.nfc.flags-aconfig", + "android.os.flags-aconfig", + "android.os.vibrator.flags-aconfig", + "android.permission.flags-aconfig", + "android.provider.flags-aconfig", + "android.security.flags-aconfig", + "android.server.app.flags-aconfig", + "android.service.autofill.flags-aconfig", + "android.service.chooser.flags-aconfig", + "android.service.controls.flags-aconfig", + "android.service.dreams.flags-aconfig", + "android.service.notification.flags-aconfig", + "android.service.voice.flags-aconfig", + "android.speech.flags-aconfig", + "android.tracing.flags-aconfig", + "android.view.accessibility.flags-aconfig", + "android.view.contentcapture.flags-aconfig", + "android.view.contentprotection.flags-aconfig", + "android.view.flags-aconfig", + "android.view.inputmethod.flags-aconfig", + "android.webkit.flags-aconfig", + "android.widget.flags-aconfig", + "camera_platform_flags", + "chre_flags", + "com.android.hardware.input.input-aconfig", + "com.android.input.flags-aconfig", + "com.android.media.flags.bettertogether-aconfig", + "com.android.net.flags-aconfig", + "com.android.server.flags.services-aconfig", + "com.android.text.flags-aconfig", + "com.android.window.flags.window-aconfig", + "device_policy_aconfig_flags", + "display_flags", + "fold_lock_setting_flags", + "framework-jobscheduler-job.flags-aconfig", + "framework_graphics_flags", + "hwui_flags", + "power_flags", + "sdk_sandbox_flags", + "surfaceflinger_flags", + "telecom_flags", + "telephony_flags", + ], +} + filegroup { name: "framework-minus-apex-aconfig-srcjars", srcs: aconfig_srcjars, @@ -961,3 +1034,16 @@ java_aconfig_library { aconfig_declarations: "android.content.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// Adaptive Auth +aconfig_declarations { + name: "android.adaptiveauth.flags-aconfig", + package: "android.adaptiveauth", + srcs: ["core/java/android/adaptiveauth/*.aconfig"], +} + +java_aconfig_library { + name: "android.adaptiveauth.flags-aconfig-java", + aconfig_declarations: "android.adaptiveauth.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/Android.bp b/Android.bp index 485f1be42017..9c56733650cb 100644 --- a/Android.bp +++ b/Android.bp @@ -216,7 +216,6 @@ java_library { "apex_aidl_interface-java", "packagemanager_aidl-java", "framework-protos", - "libtombstone_proto_java", "updatable-driver-protos", "ota_metadata_proto_java", "android.hidl.base-V1.0-java", diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp index d03bbd249b00..e7adf203334e 100644 --- a/ProtoLibraries.bp +++ b/ProtoLibraries.bp @@ -34,6 +34,7 @@ gensrcs { ":ipconnectivity-proto-src", ":libstats_atom_enum_protos", ":libstats_atom_message_protos", + ":libtombstone_proto-src", "core/proto/**/*.proto", "libs/incident/**/*.proto", ], diff --git a/Ravenwood.bp b/Ravenwood.bp index d13c4d78190c..0877bcedb609 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -97,6 +97,7 @@ android_ravenwood_libgroup { "framework-minus-apex.ravenwood", "hoststubgen-helper-runtime.ravenwood", "hoststubgen-helper-framework-runtime.ravenwood", + "core-libart-for-host", "all-updatable-modules-system-stubs", "junit", "truth", diff --git a/STABILITY_OWNERS b/STABILITY_OWNERS deleted file mode 100644 index a7ecb4dfdd44..000000000000 --- a/STABILITY_OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -gaillard@google.com - diff --git a/TEST_MAPPING b/TEST_MAPPING index d59775f4060b..c904eb46d88e 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -138,15 +138,13 @@ } ], "postsubmit-ravenwood": [ - // TODO(ravenwood) promote it to presubmit - // TODO: Enable it once the infra knows how to run it. -// { -// "name": "CtsUtilTestCasesRavenwood", -// "file_patterns": [ -// "*Ravenwood*", -// "*ravenwood*" -// ] -// } + { + "name": "CtsUtilTestCasesRavenwood", + "host": true, + "file_patterns": [ + "[Rr]avenwood" + ] + } ], "postsubmit-managedprofile-stress": [ { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 2c9af67ecdc4..44afbe6aff51 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -951,7 +951,7 @@ public final class FlexibilityController extends StateController { @VisibleForTesting static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS; @VisibleForTesting - static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 72 * HOUR_IN_MILLIS; + static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 24 * HOUR_IN_MILLIS; private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; @VisibleForTesting final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80}; diff --git a/api/Android.bp b/api/Android.bp index 1686943d08ca..b3b18b66e097 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -60,8 +60,40 @@ 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", @@ -269,6 +301,7 @@ packages_to_document = [ // classpath (or sources) somehow. stubs_defaults { name: "android-non-updatable-stubs-defaults", + defaults: ["framework-minus-apex-aconfig-declarations"], srcs: [":android-non-updatable-stub-sources"], sdk_version: "none", system_modules: "none", diff --git a/boot/Android.bp b/boot/Android.bp index c4a1139b70a4..228d060bf9cf 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -30,6 +30,7 @@ soong_config_module_type { bool_variables: [ "car_bootclasspath_fragment", "nfc_apex_bootclasspath_fragment", + "release_crashrecovery_module", ], properties: [ "fragments", @@ -165,6 +166,15 @@ custom_platform_bootclasspath { }, ], }, + release_crashrecovery_module: { + fragments: [ + // only used when crashrecovery is enabled + { + apex: "com.android.crashrecovery", + module: "com.android.crashrecovery-bootclasspath-fragment", + }, + ], + }, }, // Additional information needed by hidden api processing. diff --git a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp index 16bb896e939c..55ea15d0cdf1 100644 --- a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp +++ b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp @@ -468,9 +468,9 @@ Result<OverlayData> FabContainer::GetOverlayData(const OverlayManifestInfo& info entry.name().c_str()); const auto& res_value = entry.res_value(); result.pairs.emplace_back(OverlayData::Value{ - name, TargetValueWithConfig{.config = entry.configuration(), .value = TargetValue{ + name, TargetValueWithConfig{.value = TargetValue{ .data_type = static_cast<uint8_t>(res_value.data_type()), - .data_value = res_value.data_value()}}}); + .data_value = res_value.data_value()}, .config = entry.configuration()}}); } } } diff --git a/cmds/idmap2/libidmap2/ResourceContainer.cpp b/cmds/idmap2/libidmap2/ResourceContainer.cpp index 7869fbdb8cea..3c0e118bbfe7 100644 --- a/cmds/idmap2/libidmap2/ResourceContainer.cpp +++ b/cmds/idmap2/libidmap2/ResourceContainer.cpp @@ -227,9 +227,9 @@ Result<OverlayData> CreateResourceMapping(ResourceId id, const ZipAssetsProvider } else { overlay_data.pairs.emplace_back( OverlayData::Value{*target_resource, TargetValueWithConfig{ - .config = std::string(), .value = TargetValue{.data_type = overlay_resource->dataType, - .data_value = overlay_resource->data}}}); + .data_value = overlay_resource->data}, + .config = std::string()}}); } } @@ -268,10 +268,11 @@ struct ResState { std::unique_ptr<AssetManager2> am; ZipAssetsProvider* zip_assets; - static Result<ResState> Initialize(std::unique_ptr<ZipAssetsProvider> zip) { + static Result<ResState> Initialize(std::unique_ptr<ZipAssetsProvider> zip, + package_property_t flags) { ResState state; state.zip_assets = zip.get(); - if ((state.apk_assets = ApkAssets::Load(std::move(zip))) == nullptr) { + if ((state.apk_assets = ApkAssets::Load(std::move(zip), flags)) == nullptr) { return Error("failed to load apk asset"); } @@ -284,7 +285,7 @@ struct ResState { } state.am = std::make_unique<AssetManager2>(); - if (!state.am->SetApkAssets({state.apk_assets})) { + if (!state.am->SetApkAssets({state.apk_assets}, false)) { return Error("failed to create asset manager"); } @@ -343,8 +344,8 @@ Result<const ResState*> ApkResourceContainer::GetState() const { return state; } - auto state = - ResState::Initialize(std::move(std::get<std::unique_ptr<ZipAssetsProvider>>(state_))); + auto state = ResState::Initialize(std::move(std::get<std::unique_ptr<ZipAssetsProvider>>(state_)), + PROPERTY_OPTIMIZE_NAME_LOOKUPS); if (!state) { return state.GetError(); } diff --git a/core/api/current.txt b/core/api/current.txt index 15c054f0b5e8..40dcc427732b 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5317,7 +5317,6 @@ package android.app { ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean); ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean); ctor public AutomaticZenRule(android.os.Parcel); - method @FlaggedApi("android.app.modes_api") public boolean canUpdate(); method public int describeContents(); method public android.net.Uri getConditionId(); method @Nullable public android.content.ComponentName getConfigurationActivity(); @@ -9380,15 +9379,15 @@ package android.app.usage { method public int describeContents(); method public long getBeginTimeMillis(); method public long getEndTimeMillis(); - method @NonNull public java.util.Set<java.lang.Integer> getEventTypes(); + method @NonNull public int[] getEventTypes(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.usage.UsageEventsQuery> CREATOR; } public static final class UsageEventsQuery.Builder { ctor public UsageEventsQuery.Builder(long, long); - method @NonNull public android.app.usage.UsageEventsQuery.Builder addEventTypes(@NonNull int...); method @NonNull public android.app.usage.UsageEventsQuery build(); + method @NonNull public android.app.usage.UsageEventsQuery.Builder setEventTypes(@NonNull int...); } public final class UsageStats implements android.os.Parcelable { @@ -9415,7 +9414,7 @@ package android.app.usage { method public java.util.List<android.app.usage.ConfigurationStats> queryConfigurations(int, long, long); method public java.util.List<android.app.usage.EventStats> queryEventStats(int, long, long); method public android.app.usage.UsageEvents queryEvents(long, long); - method @FlaggedApi("android.app.usage.filter_based_event_query_api") @NonNull @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public android.app.usage.UsageEvents queryEvents(@NonNull android.app.usage.UsageEventsQuery); + method @FlaggedApi("android.app.usage.filter_based_event_query_api") @Nullable @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public android.app.usage.UsageEvents queryEvents(@NonNull android.app.usage.UsageEventsQuery); method public android.app.usage.UsageEvents queryEventsForSelf(long, long); method public java.util.List<android.app.usage.UsageStats> queryUsageStats(int, long, long); field @FlaggedApi("android.app.usage.user_interaction_type_api") public static final String EXTRA_EVENT_ACTION = "android.app.usage.extra.EVENT_ACTION"; @@ -12446,7 +12445,7 @@ package android.content.pm { method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback); method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler); method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalStatus(int, int, long, @Nullable android.app.PendingIntent) throws android.content.pm.PackageManager.NameNotFoundException; - method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender, int) throws android.content.pm.PackageManager.NameNotFoundException; + method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String, @NonNull android.content.IntentSender) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException; method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull String, @NonNull android.content.IntentSender); method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull android.content.pm.VersionedPackage, @NonNull android.content.IntentSender); @@ -12873,7 +12872,6 @@ package android.content.pm { field public static final int COMPONENT_ENABLED_STATE_DISABLED_USER = 3; // 0x3 field public static final int COMPONENT_ENABLED_STATE_ENABLED = 1; // 0x1 field @FlaggedApi("android.content.pm.archiving") public static final int DELETE_ARCHIVE = 16; // 0x10 - field @FlaggedApi("android.content.pm.archiving") public static final int DELETE_SHOW_DIALOG = 32; // 0x20 field public static final int DONT_KILL_APP = 1; // 0x1 field public static final String EXTRA_VERIFICATION_ID = "android.content.pm.extra.VERIFICATION_ID"; field public static final String EXTRA_VERIFICATION_RESULT = "android.content.pm.extra.VERIFICATION_RESULT"; @@ -13676,11 +13674,8 @@ package android.content.res { @FlaggedApi("android.content.res.font_scale_converter_public") public interface FontScaleConverter { method public float convertDpToSp(float); method public float convertSpToDp(float); - } - - @FlaggedApi("android.content.res.font_scale_converter_public") public class FontScaleConverterFactory { - method @FlaggedApi("android.content.res.font_scale_converter_public") @AnyThread @Nullable public static android.content.res.FontScaleConverter forScale(float); - method @FlaggedApi("android.content.res.font_scale_converter_public") @AnyThread public static boolean isNonLinearFontScalingActive(float); + method @AnyThread @Nullable public static android.content.res.FontScaleConverter forScale(float); + method @AnyThread public static boolean isNonLinearFontScalingActive(float); } public class ObbInfo implements android.os.Parcelable { @@ -16400,6 +16395,8 @@ package android.graphics { field public static final int START_HYPHEN_EDIT_NO_EDIT = 0; // 0x0 field public static final int STRIKE_THRU_TEXT_FLAG = 16; // 0x10 field public static final int SUBPIXEL_TEXT_FLAG = 128; // 0x80 + field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_LEFT_EDGE = 8192; // 0x2000 + field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 16384; // 0x4000 field public static final int UNDERLINE_TEXT_FLAG = 8; // 0x8 } @@ -18683,6 +18680,8 @@ package android.hardware.biometrics { method @Nullable public int getAllowedAuthenticators(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); @@ -18732,6 +18731,8 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@DrawableRes int); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@NonNull android.graphics.Bitmap); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence); @@ -18753,21 +18754,21 @@ package android.hardware.biometrics { method @Nullable public java.security.Signature getSignature(); } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentListItem { + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentItem { } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem { - ctor public PromptContentListItemBulletedText(@NonNull CharSequence); + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { + ctor public PromptContentItemBulletedText(@NonNull CharSequence); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemBulletedText> CREATOR; + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR; } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem { - ctor public PromptContentListItemPlainText(@NonNull CharSequence); + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { + ctor public PromptContentItemPlainText(@NonNull CharSequence); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemPlainText> CREATOR; + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR; } @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView { @@ -18776,7 +18777,7 @@ package android.hardware.biometrics { @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView { method public int describeContents(); method @Nullable public CharSequence getDescription(); - method @NonNull public java.util.List<android.hardware.biometrics.PromptContentListItem> getListItems(); + method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems(); method public static int getMaxEachItemCharacterNumber(); method public static int getMaxItemCount(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -18785,7 +18786,8 @@ package android.hardware.biometrics { public static final class PromptVerticalListContentView.Builder { ctor public PromptVerticalListContentView.Builder(); - method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentListItem); + method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem); + method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem, int); method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build(); method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence); } @@ -23341,6 +23343,7 @@ package android.media { field public static final String KEY_HDR10_PLUS_INFO = "hdr10-plus-info"; field public static final String KEY_HDR_STATIC_INFO = "hdr-static-info"; field public static final String KEY_HEIGHT = "height"; + field @FlaggedApi("com.android.media.codec.flags.codec_importance") public static final String KEY_IMPORTANCE = "importance"; field public static final String KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period"; field public static final String KEY_IS_ADTS = "is-adts"; field public static final String KEY_IS_AUTOSELECT = "is-autoselect"; @@ -24303,7 +24306,7 @@ package android.media { method public void release(); method public void selectRoute(@NonNull android.media.MediaRoute2Info); method public void setVolume(int); - method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferRequestedBySelf(); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferInitiatedBySelf(); } public abstract static class MediaRouter2.TransferCallback { @@ -26669,12 +26672,16 @@ package android.media.tv { public static final class TvContract.Channels implements android.media.tv.TvContract.BaseTvColumns { method @Nullable public static String getVideoResolution(String); + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; // 0x2 + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; // 0x1 + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; // 0x0 field public static final String COLUMN_APP_LINK_COLOR = "app_link_color"; field public static final String COLUMN_APP_LINK_ICON_URI = "app_link_icon_uri"; field public static final String COLUMN_APP_LINK_INTENT_URI = "app_link_intent_uri"; field public static final String COLUMN_APP_LINK_POSTER_ART_URI = "app_link_poster_art_uri"; field public static final String COLUMN_APP_LINK_TEXT = "app_link_text"; field public static final String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type"; field public static final String COLUMN_BROWSABLE = "browsable"; field public static final String COLUMN_CHANNEL_LIST_ID = "channel_list_id"; field public static final String COLUMN_DESCRIPTION = "description"; @@ -31845,6 +31852,7 @@ package android.os { method public long computeChargeTimeRemaining(); method public int getIntProperty(int); method public long getLongProperty(int); + method @FlaggedApi("android.os.battery_part_status_api") @Nullable public String getStringProperty(int); method public boolean isCharging(); field public static final String ACTION_CHARGING = "android.os.action.CHARGING"; field public static final String ACTION_DISCHARGING = "android.os.action.DISCHARGING"; @@ -43072,6 +43080,8 @@ package android.telephony { field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool"; field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool"; field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool"; field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool"; @@ -51409,7 +51419,7 @@ package android.view { method @NonNull public android.view.SurfaceControl.Transaction setCrop(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect); method @NonNull public android.view.SurfaceControl.Transaction setDamageRegion(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Region); method @NonNull public android.view.SurfaceControl.Transaction setDataSpace(@NonNull android.view.SurfaceControl, int); - method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setDesiredPresentTime(long); + method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setDesiredPresentTimeNanos(long); method @NonNull public android.view.SurfaceControl.Transaction setExtendedRangeBrightness(@NonNull android.view.SurfaceControl, float, float); method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int); method @NonNull public android.view.SurfaceControl.Transaction setFrameRate(@NonNull android.view.SurfaceControl, @FloatRange(from=0.0) float, int, int); @@ -51430,7 +51440,7 @@ package android.view { } @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public static final class SurfaceControl.TransactionStats { - method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public long getLatchTime(); + method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public long getLatchTimeNanos(); method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.hardware.SyncFence getPresentFence(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index d26662d9039d..2c01e3f42631 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -12,6 +12,7 @@ package android { field @Deprecated public static final String ACCESS_FM_RADIO = "android.permission.ACCESS_FM_RADIO"; field public static final String ACCESS_FPS_COUNTER = "android.permission.ACCESS_FPS_COUNTER"; field public static final String ACCESS_INSTANT_APPS = "android.permission.ACCESS_INSTANT_APPS"; + field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACCESS_LAST_KNOWN_CELL_ID = "android.permission.ACCESS_LAST_KNOWN_CELL_ID"; field public static final String ACCESS_LOCUS_ID_USAGE_STATS = "android.permission.ACCESS_LOCUS_ID_USAGE_STATS"; field public static final String ACCESS_MOCK_LOCATION = "android.permission.ACCESS_MOCK_LOCATION"; field public static final String ACCESS_MTP = "android.permission.ACCESS_MTP"; @@ -133,6 +134,7 @@ package android { field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA"; field public static final String GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS"; + field @FlaggedApi("android.app.bic_client") public static final String GET_BACKGROUND_INSTALLED_PACKAGES = "android.permission.GET_BACKGROUND_INSTALLED_PACKAGES"; field @FlaggedApi("android.app.get_binding_uid_importance") public static final String GET_BINDING_UID_IMPORTANCE = "android.permission.GET_BINDING_UID_IMPORTANCE"; field public static final String GET_HISTORICAL_APP_OPS_STATS = "android.permission.GET_HISTORICAL_APP_OPS_STATS"; field public static final String GET_PROCESS_STATE_AND_OOM_SCORE = "android.permission.GET_PROCESS_STATE_AND_OOM_SCORE"; @@ -294,6 +296,7 @@ package android { field public static final String READ_RUNTIME_PROFILES = "android.permission.READ_RUNTIME_PROFILES"; field public static final String READ_SAFETY_CENTER_STATUS = "android.permission.READ_SAFETY_CENTER_STATUS"; field public static final String READ_SEARCH_INDEXABLES = "android.permission.READ_SEARCH_INDEXABLES"; + field @FlaggedApi("android.app.system_terms_of_address_enabled") public static final String READ_SYSTEM_GRAMMATICAL_GENDER = "android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"; field public static final String READ_SYSTEM_UPDATE_INFO = "android.permission.READ_SYSTEM_UPDATE_INFO"; field public static final String READ_WALLPAPER_INTERNAL = "android.permission.READ_WALLPAPER_INTERNAL"; field public static final String READ_WIFI_CREDENTIAL = "android.permission.READ_WIFI_CREDENTIAL"; @@ -419,6 +422,7 @@ package android { public static final class R.attr { field public static final int allowClearUserDataOnFailedRestore = 16844288; // 0x1010600 + field @FlaggedApi("android.content.res.manifest_flagging") public static final int featureFlag; field public static final int gameSessionService = 16844373; // 0x1010655 field public static final int hotwordDetectionService = 16844326; // 0x1010626 field @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") public static final int isVirtualDeviceOnly; @@ -552,6 +556,7 @@ package android.app { public class ActivityManager { method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int); + method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @Nullable int[]); method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String); method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int); method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser(); @@ -622,6 +627,7 @@ package android.app { field public static final String OPSTR_ACCEPT_HANDOVER = "android:accept_handover"; field public static final String OPSTR_ACCESS_ACCESSIBILITY = "android:access_accessibility"; field public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications"; + field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS = "android:access_restricted_settings"; field public static final String OPSTR_ACTIVATE_PLATFORM_VPN = "android:activate_platform_vpn"; field public static final String OPSTR_ACTIVATE_VPN = "android:activate_vpn"; field public static final String OPSTR_ASSIST_SCREENSHOT = "android:assist_screenshot"; @@ -1614,6 +1620,7 @@ package android.app.ambientcontext { field public static final int LEVEL_MEDIUM_HIGH = 4; // 0x4 field public static final int LEVEL_MEDIUM_LOW = 2; // 0x2 field public static final int LEVEL_UNKNOWN = 0; // 0x0 + field @FlaggedApi("android.app.ambient_heart_rate") public static final int RATE_PER_MINUTE_UNKNOWN = -1; // 0xffffffff } public static final class AmbientContextEvent.Builder { @@ -3541,6 +3548,7 @@ package android.content { field public static final String CLOUDSEARCH_SERVICE = "cloudsearch"; field public static final String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions"; field public static final String CONTEXTHUB_SERVICE = "contexthub"; + field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation"; field public static final String ETHERNET_SERVICE = "ethernet"; field public static final String EUICC_CARD_SERVICE = "euicc_card"; field public static final String FONT_SERVICE = "font"; @@ -4219,11 +4227,14 @@ package android.content.pm { field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR; field public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; // 0x1 field public static final int CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION = 0; // 0x0 + field public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2 field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1 field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0 + field public static final int SHOW_IN_QUIET_MODE_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_SHARING_SURFACES_NO = 2; // 0x2 field public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = 1; // 0x1 + field public static final int SHOW_IN_SHARING_SURFACES_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = 0; // 0x0 } @@ -9978,6 +9989,7 @@ package android.nfc.cardemulation { @FlaggedApi("android.nfc.enable_nfc_mainline") public final class ApduServiceInfo implements android.os.Parcelable { ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilter(@NonNull String); method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream); @@ -9988,6 +10000,7 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getDescription(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.nfc.cardemulation.AidGroup getDynamicAidGroupForCategory(@NonNull String); method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public String getOffHostSecureElement(); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") @NonNull public java.util.List<java.lang.String> getPollingLoopFilters(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getPrefixAids(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getSettingsActivityName(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getSubsetAids(); @@ -10000,6 +10013,7 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.graphics.drawable.Drawable loadIcon(@NonNull android.content.pm.PackageManager); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public CharSequence loadLabel(@NonNull android.content.pm.PackageManager); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public boolean removeDynamicAidGroupForCategory(@NonNull String); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void removePollingLoopFilter(@NonNull String); method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresScreenOn(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresUnlock(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void resetOffHostSecureElement(); @@ -10038,12 +10052,17 @@ package android.os { field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_CHARGING_POLICY = 9; // 0x9 field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_FIRST_USAGE_DATE = 8; // 0x8 field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_MANUFACTURING_DATE = 7; // 0x7 + field @FlaggedApi("android.os.battery_part_status_api") @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_PART_STATUS = 12; // 0xc + field @FlaggedApi("android.os.battery_part_status_api") @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_SERIAL_NUMBER = 11; // 0xb field public static final int CHARGING_POLICY_ADAPTIVE_AC = 3; // 0x3 field public static final int CHARGING_POLICY_ADAPTIVE_AON = 2; // 0x2 field public static final int CHARGING_POLICY_ADAPTIVE_LONGLIFE = 4; // 0x4 field public static final int CHARGING_POLICY_DEFAULT = 1; // 0x1 field public static final String EXTRA_EVENTS = "android.os.extra.EVENTS"; field public static final String EXTRA_EVENT_TIMESTAMP = "android.os.extra.EVENT_TIMESTAMP"; + field @FlaggedApi("android.os.battery_part_status_api") public static final int PART_STATUS_ORIGINAL = 1; // 0x1 + field @FlaggedApi("android.os.battery_part_status_api") public static final int PART_STATUS_REPLACED = 2; // 0x2 + field @FlaggedApi("android.os.battery_part_status_api") public static final int PART_STATUS_UNSUPPORTED = 0; // 0x0 } public final class BatterySaverPolicyConfig implements android.os.Parcelable { @@ -14554,6 +14573,7 @@ package android.telephony { field public static final int EVENT_SERVICE_STATE_CHANGED = 1; // 0x1 field public static final int EVENT_SIGNAL_STRENGTHS_CHANGED = 9; // 0x9 field public static final int EVENT_SIGNAL_STRENGTH_CHANGED = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; // 0x29 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SRVCC_STATE_CHANGED = 16; // 0x10 field public static final int EVENT_USER_MOBILE_DATA_STATE_CHANGED = 20; // 0x14 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_VOICE_ACTIVATION_STATE_CHANGED = 18; // 0x12 @@ -14600,6 +14620,10 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onRadioPowerStateChanged(int); } + @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") public static interface TelephonyCallback.SimultaneousCellularCallingSupportListener { + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSimultaneousCellularCallingSubscriptionsChanged(@NonNull java.util.Set<java.lang.Integer>); + } + public static interface TelephonyCallback.SrvccStateListener { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSrvccStateChanged(int); } @@ -14675,6 +14699,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst(); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) public android.telephony.CellIdentity getLastKnownCellIdentity(); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Map<java.lang.Integer,java.lang.Integer> getLogicalToPhysicalSlotMapping(); method public int getMaxNumberOfSimultaneouslyActiveSims(); method public static long getMaxNumberVerificationTimeoutMillis(); @@ -14778,6 +14803,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setSystemSelectionChannels(@NonNull java.util.List<android.telephony.RadioAccessSpecifier>); method @Deprecated public void setVisualVoicemailEnabled(android.telecom.PhoneAccountHandle, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setVoiceActivationState(int); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(android.Manifest.permission.BIND_TELECOM_CONNECTION_SERVICE) public void setVoiceServiceStateOverride(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void shutdownAllRadios(); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult supplyIccLockPin(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult supplyIccLockPuk(@NonNull String, @NonNull String); @@ -14917,6 +14943,11 @@ package android.telephony { field public static final int ERROR_UNKNOWN = 0; // 0x0 } + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public class TelephonyRegistryManager { + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyCallStateChangedForAllSubscriptions(int, @Nullable String); + method public void notifyOutgoingEmergencyCall(int, int, @NonNull android.telephony.emergency.EmergencyNumber); + } + public final class ThermalMitigationRequest implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.telephony.DataThrottlingRequest getDataThrottlingRequest(); @@ -17154,6 +17185,7 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_NR_NTN = 2; // 0x2 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_PROPRIETARY = 4; // 0x4 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; // 0x2 field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; // 0x1 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; // 0x7 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index bbe03a3d11a2..a840fc2b394f 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -284,16 +284,6 @@ package android.app { method public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, boolean, int, int); } - public final class AutomaticZenRule implements android.os.Parcelable { - method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields(); - field @FlaggedApi("android.app.modes_api") public static final int FIELD_INTERRUPTION_FILTER = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_NAME = 1; // 0x1 - } - - @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder { - method @FlaggedApi("android.app.modes_api") @NonNull public android.app.AutomaticZenRule.Builder setUserModifiedFields(int); - } - public class BroadcastOptions extends android.app.ComponentOptions { ctor public BroadcastOptions(); ctor public BroadcastOptions(@NonNull android.os.Bundle); @@ -493,10 +483,15 @@ package android.app { } public class UiModeManager { + method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay(); method public boolean isNightModeLocked(); method public boolean isUiModeLocked(); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int); + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9 + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8 + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1 field public static final int PROJECTION_TYPE_NONE = 0; // 0x0 @@ -1177,6 +1172,7 @@ package android.content.pm { method public int getShowInLauncher(); field public static final int SHOW_IN_LAUNCHER_NO = 2; // 0x2 field public static final int SHOW_IN_LAUNCHER_SEPARATE = 1; // 0x1 + field public static final int SHOW_IN_LAUNCHER_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0; // 0x0 } @@ -3021,47 +3017,8 @@ package android.service.notification { method @Deprecated public boolean isBound(); } - @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable { - method public int getUserModifiedFields(); - field public static final int FIELD_DIM_WALLPAPER = 4; // 0x4 - field public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 16; // 0x10 - field public static final int FIELD_DISABLE_TAP_TO_WAKE = 32; // 0x20 - field public static final int FIELD_DISABLE_TILT_TO_WAKE = 64; // 0x40 - field public static final int FIELD_DISABLE_TOUCH = 128; // 0x80 - field public static final int FIELD_GRAYSCALE = 1; // 0x1 - field public static final int FIELD_MAXIMIZE_DOZE = 512; // 0x200 - field public static final int FIELD_MINIMIZE_RADIO_USAGE = 256; // 0x100 - field public static final int FIELD_NIGHT_MODE = 8; // 0x8 - field public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 2; // 0x2 - } - - @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder { - method @NonNull public android.service.notification.ZenDeviceEffects.Builder setUserModifiedFields(int); - } - - public final class ZenPolicy implements android.os.Parcelable { - method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields(); - field @FlaggedApi("android.app.modes_api") public static final int FIELD_ALLOW_CHANNELS = 8; // 0x8 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_CALLS = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_CONVERSATIONS = 4; // 0x4 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_MESSAGES = 1; // 0x1 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 128; // 0x80 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 32; // 0x20 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 256; // 0x100 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 64; // 0x40 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 512; // 0x200 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_AMBIENT = 32768; // 0x8000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_BADGE = 16384; // 0x4000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1024; // 0x400 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_LIGHTS = 2048; // 0x800 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 65536; // 0x10000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_PEEK = 4096; // 0x1000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 8192; // 0x2000 - } - public static final class ZenPolicy.Builder { ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy); - method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder setUserModifiedFields(int); } } @@ -3292,7 +3249,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void refreshUiccProfile(); method @Deprecated public void setCarrierTestOverride(String, String, String, String, String, String, String); method public void setCarrierTestOverride(String, String, String, String, String, String, String, String, String); - method @RequiresPermission(android.Manifest.permission.BIND_TELECOM_CONNECTION_SERVICE) public void setVoiceServiceStateOverride(boolean); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(android.Manifest.permission.BIND_TELECOM_CONNECTION_SERVICE) public void setVoiceServiceStateOverride(boolean); field public static final int HAL_SERVICE_DATA = 1; // 0x1 field public static final int HAL_SERVICE_IMS = 7; // 0x7 field public static final int HAL_SERVICE_MESSAGING = 2; // 0x2 diff --git a/core/java/android/adaptiveauth/OWNERS b/core/java/android/adaptiveauth/OWNERS new file mode 100644 index 000000000000..0218a7835586 --- /dev/null +++ b/core/java/android/adaptiveauth/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file diff --git a/core/java/android/adaptiveauth/flags.aconfig b/core/java/android/adaptiveauth/flags.aconfig new file mode 100644 index 000000000000..39e46bbdfa6a --- /dev/null +++ b/core/java/android/adaptiveauth/flags.aconfig @@ -0,0 +1,8 @@ +package: "android.adaptiveauth" + +flag { + name: "report_biometric_auth_attempts" + namespace: "biometrics" + description: "Control the usage of the biometric auth signal in adaptive auth" + bug: "285053096" +}
\ No newline at end of file diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index ebb5ba0e7b0f..5318bb722b5f 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -3574,7 +3574,7 @@ public class ActivityManager { * foreground. This may be running a window that is behind the current * foreground (so paused and with its state saved, not interacting with * the user, but visible to them to some degree); it may also be running - * other services under the system's control that it inconsiders important. + * other services under the system's control that it considers important. */ public static final int IMPORTANCE_VISIBLE = 200; @@ -3646,9 +3646,9 @@ public class ActivityManager { public static final int IMPORTANCE_CANT_SAVE_STATE = 350; /** - * Constant for {@link #importance}: This process process contains - * cached code that is expendable, not actively running any app components - * we care about. + * Constant for {@link #importance}: This process contains cached code + * that is expendable, not actively running any app components we care + * about. */ public static final int IMPORTANCE_CACHED = 400; @@ -4402,7 +4402,7 @@ public class ActivityManager { } /** - * Start monitoring changes to the importance of uids running in the system. + * Start monitoring changes to the importance of all uids running in the system. * @param listener The listener callback that will receive change reports. * @param importanceCutpoint The level of importance in which the caller is interested * in differences. For example, if {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} @@ -4422,17 +4422,48 @@ public class ActivityManager { @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(OnUidImportanceListener listener, @RunningAppProcessInfo.Importance int importanceCutpoint) { - synchronized (this) { + addOnUidImportanceListenerInternal(listener, importanceCutpoint, null /* uids */); + } + + /** + * Start monitoring changes to the importance of given uids running in the system. + * + * @param listener The listener callback that will receive change reports. + * @param importanceCutpoint The level of importance in which the caller is interested + * in differences. For example, if {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} + * is used here, you will receive a call each time a uids importance transitions between + * being <= {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} and + * > {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}. + * @param uids The UIDs that this listener is interested with. A {@code null} value means + * all UIDs will be monitored by this listener, this will be equivalent to the + * {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case. + * + * @throws IllegalArgumentException If the listener is already registered. + * @hide + */ + @FlaggedApi(Flags.FLAG_UID_IMPORTANCE_LISTENER_FOR_UIDS) + @SystemApi + @SuppressLint("SamShouldBeLast") + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) + public void addOnUidImportanceListener(@NonNull OnUidImportanceListener listener, + @RunningAppProcessInfo.Importance int importanceCutpoint, @Nullable int[] uids) { + addOnUidImportanceListenerInternal(listener, importanceCutpoint, uids); + } + + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) + private void addOnUidImportanceListenerInternal(@NonNull OnUidImportanceListener listener, + @RunningAppProcessInfo.Importance int importanceCutpoint, @Nullable int[] uids) { + synchronized (mImportanceListeners) { if (mImportanceListeners.containsKey(listener)) { throw new IllegalArgumentException("Listener already registered: " + listener); } // TODO: implement the cut point in the system process to avoid IPCs. MyUidObserver observer = new MyUidObserver(listener, mContext); try { - getService().registerUidObserver(observer, + getService().registerUidObserverForUids(observer, UID_OBSERVER_PROCSTATE | UID_OBSERVER_GONE, RunningAppProcessInfo.importanceToProcState(importanceCutpoint), - mContext.getOpPackageName()); + mContext.getOpPackageName(), uids); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -4450,7 +4481,7 @@ public class ActivityManager { @SystemApi @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public void removeOnUidImportanceListener(OnUidImportanceListener listener) { - synchronized (this) { + synchronized (mImportanceListeners) { MyUidObserver observer = mImportanceListeners.remove(listener); if (observer == null) { throw new IllegalArgumentException("Listener not registered: " + listener); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 1db1caf51800..4b2e93fda171 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1540,9 +1540,16 @@ public class AppOpsManager { public static final int OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER = AppProtoEnums.APP_OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER; + /** + * See {@link #OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER}. + * @hide + */ + public static final int OP_READ_SYSTEM_GRAMMATICAL_GENDER = + AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 143; + public static final int _NUM_OP = 144; /** * All app ops represented as strings. @@ -2180,6 +2187,8 @@ public class AppOpsManager { * * @hide */ + @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED) + @SystemApi public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS = "android:access_restricted_settings"; @@ -2373,6 +2382,14 @@ public class AppOpsManager { public static final String OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER = "android:rapid_clear_notifications_by_listener"; + /** + * Allows an application to read the system grammatical gender. + * + * @hide + */ + public static final String OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER = + "android:read_system_grammatical_gender"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2484,6 +2501,7 @@ public class AppOpsManager { OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OP_MEDIA_ROUTING_CONTROL, + OP_READ_SYSTEM_GRAMMATICAL_GENDER, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2936,6 +2954,10 @@ public class AppOpsManager { OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, "RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER") .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_READ_SYSTEM_GRAMMATICAL_GENDER, + OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER, "READ_SYSTEM_GRAMMATICAL_GENDER") + .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER) + .build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index cc3eac1bbf01..afa513dbaaef 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -26,11 +26,19 @@ import android.icu.text.SimpleDateFormat; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; +import android.util.Xml; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.WireTypeMismatchException; +import com.android.internal.util.XmlUtils; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + +import org.xmlpull.v1.XmlPullParserException; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -40,6 +48,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; +import java.util.Objects; /** * Describes information related to an application process's startup. @@ -215,6 +224,11 @@ public final class ApplicationStartInfo implements Parcelable { private int mDefiningUid; /** + * @see #getPackageName + */ + private String mPackageName; + + /** * @see #getProcessName */ private String mProcessName; @@ -349,6 +363,14 @@ public final class ApplicationStartInfo implements Parcelable { } /** + * @see #getPackageName + * @hide + */ + public void setPackageName(final String packageName) { + mPackageName = intern(packageName); + } + + /** * @see #getProcessName * @hide */ @@ -461,6 +483,15 @@ public final class ApplicationStartInfo implements Parcelable { } /** + * Name of first package running in this process; + * + * @hide + */ + public String getPackageName() { + return mPackageName; + } + + /** * The actual process name it was running with. * * <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p> @@ -555,6 +586,7 @@ public final class ApplicationStartInfo implements Parcelable { dest.writeInt(mRealUid); dest.writeInt(mPackageUid); dest.writeInt(mDefiningUid); + dest.writeString(mPackageName); dest.writeString(mProcessName); dest.writeInt(mReason); dest.writeInt(mStartupTimestampsNs == null ? 0 : mStartupTimestampsNs.size()); @@ -579,6 +611,7 @@ public final class ApplicationStartInfo implements Parcelable { mRealUid = other.mRealUid; mPackageUid = other.mPackageUid; mDefiningUid = other.mDefiningUid; + mPackageName = other.mPackageName; mProcessName = other.mProcessName; mReason = other.mReason; mStartupTimestampsNs = other.mStartupTimestampsNs; @@ -593,6 +626,7 @@ public final class ApplicationStartInfo implements Parcelable { mRealUid = in.readInt(); mPackageUid = in.readInt(); mDefiningUid = in.readInt(); + mPackageName = intern(in.readString()); mProcessName = intern(in.readString()); mReason = in.readInt(); int starupTimestampCount = in.readInt(); @@ -624,6 +658,11 @@ public final class ApplicationStartInfo implements Parcelable { } }; + private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS = "timestamps"; + private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP = "timestamp"; + private static final String PROTO_SERIALIZER_ATTRIBUTE_KEY = "key"; + private static final String PROTO_SERIALIZER_ATTRIBUTE_TS = "ts"; + /** * Write to a protocol buffer output stream. Protocol buffer message definition at {@link * android.app.ApplicationStartInfoProto} @@ -644,9 +683,22 @@ public final class ApplicationStartInfo implements Parcelable { if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) { ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream(); ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes); - timestampsOut.writeObject(mStartupTimestampsNs); + TypedXmlSerializer serializer = Xml.resolveSerializer(timestampsOut); + serializer.startDocument(null, true); + serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + for (int i = 0; i < mStartupTimestampsNs.size(); i++) { + serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + serializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY, + mStartupTimestampsNs.keyAt(i)); + serializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS, + mStartupTimestampsNs.valueAt(i)); + serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + } + serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + serializer.endDocument(); proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS, timestampsBytes.toByteArray()); + timestampsOut.close(); } proto.write(ApplicationStartInfoProto.START_TYPE, mStartType); if (mStartIntent != null) { @@ -697,7 +749,24 @@ public final class ApplicationStartInfo implements Parcelable { ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes( ApplicationStartInfoProto.STARTUP_TIMESTAMPS)); ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes); - mStartupTimestampsNs = (ArrayMap<Integer, Long>) timestampsIn.readObject(); + mStartupTimestampsNs = new ArrayMap<Integer, Long>(); + try { + TypedXmlPullParser parser = Xml.resolvePullParser(timestampsIn); + XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(parser.getName())) { + int key = parser.getAttributeInt(null, + PROTO_SERIALIZER_ATTRIBUTE_KEY); + long ts = parser.getAttributeLong(null, + PROTO_SERIALIZER_ATTRIBUTE_TS); + mStartupTimestampsNs.put(key, ts); + } + } + } catch (XmlPullParserException e) { + // Timestamps lost + } + timestampsIn.close(); break; case (int) ApplicationStartInfoProto.START_TYPE: mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE); @@ -734,6 +803,7 @@ public final class ApplicationStartInfo implements Parcelable { .append(" definingUid=").append(mDefiningUid) .append(" user=").append(UserHandle.getUserId(mPackageUid)) .append('\n') + .append(" package=").append(mPackageName) .append(" process=").append(mProcessName) .append(" startupState=").append(mStartupState) .append(" reason=").append(reasonToString(mReason)) @@ -782,4 +852,35 @@ public final class ApplicationStartInfo implements Parcelable { default -> ""; }; } + + /** @hide */ + @Override + public boolean equals(@Nullable Object other) { + if (other == null || !(other instanceof ApplicationStartInfo)) { + return false; + } + final ApplicationStartInfo o = (ApplicationStartInfo) other; + return mPid == o.mPid && mRealUid == o.mRealUid && mPackageUid == o.mPackageUid + && mDefiningUid == o.mDefiningUid && mReason == o.mReason + && mStartupState == o.mStartupState && mStartType == o.mStartType + && mLaunchMode == o.mLaunchMode && TextUtils.equals(mProcessName, o.mProcessName) + && timestampsEquals(o); + } + + @Override + public int hashCode() { + return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState, + mStartType, mLaunchMode, mProcessName, + mStartupTimestampsNs); + } + + private boolean timestampsEquals(@NonNull ApplicationStartInfo other) { + if (mStartupTimestampsNs == null && other.mStartupTimestampsNs == null) { + return true; + } + if (mStartupTimestampsNs == null || other.mStartupTimestampsNs == null) { + return false; + } + return mStartupTimestampsNs.equals(other.mStartupTimestampsNs); + } } diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index 5b354fc3b9ed..d57a4e583a1a 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -23,7 +23,6 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.TestApi; import android.app.NotificationManager.InterruptionFilter; import android.content.ComponentName; import android.net.Uri; @@ -113,8 +112,8 @@ public final class AutomaticZenRule implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface Type {} - /** Used to track which rule variables have been modified by the user. - * Should be checked against the bitmask {@link #getUserModifiedFields()}. + /** + * Enum for the user-modifiable fields in this object. * @hide */ @IntDef(flag = true, prefix = { "FIELD_" }, value = { @@ -128,13 +127,11 @@ public final class AutomaticZenRule implements Parcelable { * @hide */ @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi public static final int FIELD_NAME = 1 << 0; /** * @hide */ @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi public static final int FIELD_INTERRUPTION_FILTER = 1 << 1; private boolean enabled; @@ -153,7 +150,6 @@ public final class AutomaticZenRule implements Parcelable { private int mIconResId; private String mTriggerDescription; private boolean mAllowManualInvocation; - private @ModifiableField int mUserModifiedFields; // Bitwise representation /** * The maximum string length for any string contained in this automatic zen rule. This pertains @@ -256,7 +252,6 @@ public final class AutomaticZenRule implements Parcelable { mIconResId = source.readInt(); mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); mType = source.readInt(); - mUserModifiedFields = source.readInt(); } } @@ -307,8 +302,7 @@ public final class AutomaticZenRule implements Parcelable { * Returns whether this rule's name has been modified by the user. * @hide */ - // TODO: b/310620812 - Replace with mUserModifiedFields & FIELD_NAME once - // FLAG_MODES_API is inlined. + // TODO: b/310620812 - Consider removing completely. Seems not be used anywhere except tests. public boolean isModified() { return mModified; } @@ -506,32 +500,6 @@ public final class AutomaticZenRule implements Parcelable { return type; } - /** - * Gets the bitmask representing which fields are user modified. Bits are set using - * {@link ModifiableField}. - * @hide - */ - @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi - public @ModifiableField int getUserModifiedFields() { - return mUserModifiedFields; - } - - /** - * Returns {@code true} if the {@link AutomaticZenRule} can be updated. - * When this returns {@code false}, calls to - * {@link NotificationManager#updateAutomaticZenRule(String, AutomaticZenRule)}) with this rule - * will ignore changes to user-configurable fields. - */ - @FlaggedApi(Flags.FLAG_MODES_API) - public boolean canUpdate() { - // The rule is considered updateable if its bitmask has no user modifications, and - // the bitmasks of the policy and device effects have no modification. - return mUserModifiedFields == 0 - && (mZenPolicy == null || mZenPolicy.getUserModifiedFields() == 0) - && (mDeviceEffects == null || mDeviceEffects.getUserModifiedFields() == 0); - } - @Override public int describeContents() { return 0; @@ -560,7 +528,6 @@ public final class AutomaticZenRule implements Parcelable { dest.writeInt(mIconResId); dest.writeString(mTriggerDescription); dest.writeInt(mType); - dest.writeInt(mUserModifiedFields); } } @@ -582,16 +549,14 @@ public final class AutomaticZenRule implements Parcelable { .append(",allowManualInvocation=").append(mAllowManualInvocation) .append(",iconResId=").append(mIconResId) .append(",triggerDescription=").append(mTriggerDescription) - .append(",type=").append(mType) - .append(",userModifiedFields=") - .append(modifiedFieldsToString(mUserModifiedFields)); + .append(",type=").append(mType); } return sb.append(']').toString(); } - @FlaggedApi(Flags.FLAG_MODES_API) - private String modifiedFieldsToString(int bitmask) { + /** @hide */ + public static String fieldsToString(@ModifiableField int bitmask) { ArrayList<String> modified = new ArrayList<>(); if ((bitmask & FIELD_NAME) != 0) { modified.add("FIELD_NAME"); @@ -623,8 +588,7 @@ public final class AutomaticZenRule implements Parcelable { && other.mAllowManualInvocation == mAllowManualInvocation && other.mIconResId == mIconResId && Objects.equals(other.mTriggerDescription, mTriggerDescription) - && other.mType == mType - && other.mUserModifiedFields == mUserModifiedFields; + && other.mType == mType; } return finalEquals; } @@ -634,8 +598,7 @@ public final class AutomaticZenRule implements Parcelable { if (Flags.modesApi()) { return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime, - mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType, - mUserModifiedFields); + mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType); } return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mModified, creationTime, mPkg); @@ -704,7 +667,6 @@ public final class AutomaticZenRule implements Parcelable { private boolean mAllowManualInvocation; private long mCreationTime; private String mPkg; - private @ModifiableField int mUserModifiedFields; public Builder(@NonNull AutomaticZenRule rule) { mName = rule.getName(); @@ -721,7 +683,6 @@ public final class AutomaticZenRule implements Parcelable { mAllowManualInvocation = rule.isManualInvocationAllowed(); mCreationTime = rule.getCreationTime(); mPkg = rule.getPackageName(); - mUserModifiedFields = rule.mUserModifiedFields; } public Builder(@NonNull String name, @NonNull Uri conditionId) { @@ -848,19 +809,6 @@ public final class AutomaticZenRule implements Parcelable { return this; } - /** - * Sets the bitmask representing which fields have been user-modified. - * This method should not be used outside of tests. The value of userModifiedFields - * should be set based on what values are changed when a rule is populated or updated.. - * @hide - */ - @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi - public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { - mUserModifiedFields = userModifiedFields; - return this; - } - public @NonNull AutomaticZenRule build() { AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity, mConditionId, mPolicy, mInterruptionFilter, mEnabled); @@ -871,7 +819,6 @@ public final class AutomaticZenRule implements Parcelable { rule.mIconResId = mIconResId; rule.mAllowManualInvocation = mAllowManualInvocation; rule.setPackageName(mPkg); - rule.mUserModifiedFields = mUserModifiedFields; return rule; } diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java index a55121aaa12c..483a6e11a42a 100644 --- a/core/java/android/app/GrammaticalInflectionManager.java +++ b/core/java/android/app/GrammaticalInflectionManager.java @@ -114,7 +114,7 @@ public class GrammaticalInflectionManager { } try { - mService.setSystemWideGrammaticalGender(mContext.getUserId(), grammaticalGender); + mService.setSystemWideGrammaticalGender(grammaticalGender, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -131,7 +131,8 @@ public class GrammaticalInflectionManager { @Configuration.GrammaticalGender public int getSystemGrammaticalGender() { try { - return mService.getSystemGrammaticalGender(mContext.getUserId()); + return mService.getSystemGrammaticalGender(mContext.getAttributionSource(), + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index b5d88e878d8d..47403d26c907 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -116,6 +116,7 @@ interface IActivityManager { * @throws RemoteException * @return Returns A binder token identifying the UidObserver registration. */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") IBinder registerUidObserverForUids(in IUidObserver observer, int which, int cutpoint, String callingPackage, in int[] uids); diff --git a/core/java/android/app/IGrammaticalInflectionManager.aidl b/core/java/android/app/IGrammaticalInflectionManager.aidl index 48a48416d592..86f2e9110889 100644 --- a/core/java/android/app/IGrammaticalInflectionManager.aidl +++ b/core/java/android/app/IGrammaticalInflectionManager.aidl @@ -1,5 +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 android.app; +import android.content.AttributionSource; /** * Internal interface used to control app-specific gender. @@ -20,10 +37,10 @@ package android.app; /** * Sets the grammatical gender to system. */ - void setSystemWideGrammaticalGender(int userId, int gender); + void setSystemWideGrammaticalGender(int gender, int userId); /** * Gets the grammatical gender from system. */ - int getSystemGrammaticalGender(int userId); + int getSystemGrammaticalGender(in AttributionSource attributionSource, int userId); } diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index 60b34cdc1b8a..3b83024d536b 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -95,6 +95,34 @@ interface IUiModeManager { int getNightModeCustomType(); /** + * Overlays current Night Mode value. + * {@code attentionModeThemeOverlayType}. + * + * @param attentionModeThemeOverlayType + * @hide + */ + @EnforcePermission("MODIFY_DAY_NIGHT_MODE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)") + void setAttentionModeThemeOverlay(int attentionModeThemeOverlayType); + + + /** + * Returns current Attention Mode overlay type. + * <p> + * returns + * <ul> + * <li>{@link #MODE_ATTENTION_OFF}</li> + * <li>{@link #MODE_ATTENTION_NIGHT}</li> + * <li>{@link #MODE_ATTENTION_DAY}</li> + * </ul> + * </p> + * @hide + */ + @EnforcePermission("MODIFY_DAY_NIGHT_MODE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)") + int getAttentionModeThemeOverlay(); + + /** * Sets the dark mode for the given application. This setting is persisted and will override the * system configuration for this application. * 1 - notnight mode diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 476232cb40b3..ed0cfbe3d9c3 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5652,7 +5652,7 @@ public class Notification implements Parcelable pillColor = Colors.flattenAlpha( getColors(p).getTertiaryFixedDimAccentColor(), bgColor); textColor = Colors.flattenAlpha( - getColors(p).getOnTertiaryAccentTextColor(), pillColor); + getColors(p).getOnTertiaryFixedAccentTextColor(), pillColor); } contentView.setInt(R.id.expand_button, "setHighlightTextColor", textColor); contentView.setInt(R.id.expand_button, "setHighlightPillColor", pillColor); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 9cf732abb86a..d7554137fa5b 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -31,6 +31,7 @@ import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; import android.app.contentsuggestions.ContentSuggestionsManager; import android.app.contentsuggestions.IContentSuggestionsManager; +import android.app.ecm.EnhancedConfirmationFrameworkInitializer; import android.app.job.JobSchedulerFrameworkInitializer; import android.app.people.PeopleManager; import android.app.prediction.AppPredictionManager; @@ -1631,6 +1632,9 @@ public final class SystemServiceRegistry { OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); DeviceLockFrameworkInitializer.registerServiceWrappers(); VirtualizationFrameworkInitializer.registerServiceWrappers(); + if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) { + EnhancedConfirmationFrameworkInitializer.registerServiceWrappers(); + } } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 0ccb9cddf58d..a27132872521 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; @@ -265,6 +266,60 @@ public class UiModeManager { */ public static final int MODE_NIGHT_YES = 2; + /** @hide */ + @IntDef(prefix = { "MODE_ATTENTION_THEME_OVERLAY_" }, value = { + MODE_ATTENTION_THEME_OVERLAY_OFF, + MODE_ATTENTION_THEME_OVERLAY_NIGHT, + MODE_ATTENTION_THEME_OVERLAY_DAY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AttentionModeThemeOverlayType {} + + /** @hide */ + @IntDef(prefix = { "MODE_ATTENTION_THEME_OVERLAY_" }, value = { + MODE_ATTENTION_THEME_OVERLAY_OFF, + MODE_ATTENTION_THEME_OVERLAY_NIGHT, + MODE_ATTENTION_THEME_OVERLAY_DAY, + MODE_ATTENTION_THEME_OVERLAY_UNKNOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AttentionModeThemeOverlayReturnType {} + + /** + * Constant for {@link #setAttentionModeThemeOverlay(int)} (int)} and {@link + * #getAttentionModeThemeOverlay()}: Keeps night mode as set by {@link #setNightMode(int)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; + + /** + * Constant for {@link #setAttentionModeThemeOverlay(int)} (int)} and {@link + * #getAttentionModeThemeOverlay()}: Maintains night mode always on. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; + + /** + * Constant for {@link #setAttentionModeThemeOverlay(int)} (int)} and {@link + * #getAttentionModeThemeOverlay()}: Maintains night mode always off (Light). + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; + + /** + * Constant for {@link #getAttentionModeThemeOverlay()}: Error communication with server. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; + /** * Granular types for {@link #setNightModeCustomType(int)} * @hide @@ -733,6 +788,55 @@ public class UiModeManager { } /** + * Overlays current Attention mode Night Mode overlay. + * {@code attentionModeThemeOverlayType}. + * + * @throws IllegalArgumentException if passed an unsupported type to + * {@code AttentionModeThemeOverlayType}. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + public void setAttentionModeThemeOverlay( + @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) { + if (sGlobals != null) { + try { + sGlobals.mService.setAttentionModeThemeOverlay(attentionModeThemeOverlayType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Returns the currently configured Attention Mode theme overlay. + * <p> + * May be one of: + * <ul> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_OFF}</li> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_NIGHT}</li> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_DAY}</li> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_UNKNOWN}</li> + * </ul> + * </p> + * + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + public @AttentionModeThemeOverlayReturnType int getAttentionModeThemeOverlay() { + if (sGlobals != null) { + try { + return sGlobals.mService.getAttentionModeThemeOverlay(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return MODE_ATTENTION_THEME_OVERLAY_UNKNOWN; + } + + /** * Sets and persist the night mode for this application. * <p> * The mode can be one of: diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index 4fc25fd8c699..c0b299b77806 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -19,4 +19,11 @@ flag { name: "app_restrictions_api" description: "API to track and query restrictions applied to apps" bug: "320150834" -}
\ No newline at end of file +} + +flag { + namespace: "backstage_power" + name: "uid_importance_listener_for_uids" + description: "API to add OnUidImportanceListener with targetted UIDs" + bug: "286258140" +} diff --git a/core/java/android/app/ambientcontext/AmbientContextEvent.java b/core/java/android/app/ambientcontext/AmbientContextEvent.java index f94987e8495a..5ab7991c6326 100644 --- a/core/java/android/app/ambientcontext/AmbientContextEvent.java +++ b/core/java/android/app/ambientcontext/AmbientContextEvent.java @@ -89,8 +89,11 @@ public final class AmbientContextEvent implements Parcelable { */ public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name"; - /** Default value for the rate per minute data field. */ - private static final int RATE_PER_MINUTE_UNKNOWN = -1; + /** + * Default value for {@link #getRatePerMinute}. Indicates that the rate of the event is unknown. + */ + @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) + public static final int RATE_PER_MINUTE_UNKNOWN = -1; /** @hide */ @IntDef(prefix = { "EVENT_" }, value = { @@ -636,10 +639,10 @@ public final class AmbientContextEvent implements Parcelable { } @DataClass.Generated( - time = 1704895515931L, + time = 1705575046107L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java", - inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\nprivate static final int RATE_PER_MINUTE_UNKNOWN\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nprivate static int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)") + inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final @android.annotation.FlaggedApi int RATE_PER_MINUTE_UNKNOWN\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nprivate static int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/app/usage/UsageEventsQuery.java b/core/java/android/app/usage/UsageEventsQuery.java index 3cd292392694..df6324f744a8 100644 --- a/core/java/android/app/usage/UsageEventsQuery.java +++ b/core/java/android/app/usage/UsageEventsQuery.java @@ -29,9 +29,6 @@ import android.util.ArraySet; import com.android.internal.util.ArrayUtils; import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; /** * An Object-Oriented representation for a {@link UsageEvents} query. @@ -77,19 +74,17 @@ public final class UsageEventsQuery implements Parcelable { } /** - * Returns the set of usage event types for the query. - * <em>Note: An empty set indicates query for all usage events. </em> + * Retrieves the usage event types for the query. + * <p>Note that an empty array indicates querying all usage event types, and it may + * cause additional system overhead when calling + * {@link UsageStatsManager#queryEvents(UsageEventsQuery)}. Apps are encouraged to + * provide a list of event types via {@link Builder#setEventTypes(int...)}</p> + * + * @return an array contains the usage event types that was previously set using + * {@link Builder#setEventTypes(int...)} or an empty array if no value has been set. */ - public @NonNull Set<Integer> getEventTypes() { - if (ArrayUtils.isEmpty(mEventTypes)) { - return Collections.emptySet(); - } - - HashSet<Integer> eventTypeSet = new HashSet<>(); - for (int eventType : mEventTypes) { - eventTypeSet.add(eventType); - } - return eventTypeSet; + public @NonNull @Event.EventType int[] getEventTypes() { + return Arrays.copyOf(mEventTypes, mEventTypes.length); } /** @hide */ @@ -125,11 +120,6 @@ public final class UsageEventsQuery implements Parcelable { } }; - /** @hide */ - public int[] getEventTypeFilter() { - return Arrays.copyOf(mEventTypes, mEventTypes.length); - } - /** * Builder for UsageEventsQuery. */ @@ -166,12 +156,25 @@ public final class UsageEventsQuery implements Parcelable { } /** - * Specifies the list of usage event types to be included in the query. - * @param eventTypes List of the usage event types. See {@link UsageEvents.Event} + * Sets the list of usage event types to be included in the query. + * + * <p>Note: </p> An empty array will be returned by + * {@link UsageEventsQuery#getEventTypes()} without calling this method, which indicates + * querying for all event types. Apps are encouraged to provide a list of event types. + * Only the matching types supplied will be used to query. * - * @throws llegalArgumentException if the event type is not valid. + * @param eventTypes the array of the usage event types. See {@link UsageEvents.Event}. + * @throws NullPointerException if {@code eventTypes} is {@code null} or empty. + * @throws IllegalArgumentException if any of event types are invalid. + * @see UsageEventsQuery#getEventTypes() + * @see UsageStatsManager#queryEvents(UsageEventsQuery) */ - public @NonNull Builder addEventTypes(@NonNull @Event.EventType int... eventTypes) { + public @NonNull Builder setEventTypes(@NonNull @Event.EventType int... eventTypes) { + if (eventTypes == null || eventTypes.length == 0) { + throw new NullPointerException("eventTypes is null or empty"); + } + + mEventTypes.clear(); for (int i = 0; i < eventTypes.length; i++) { final int eventType = eventTypes[i]; if (eventType < Event.NONE || eventType > Event.MAX_EVENT_TYPE) { diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 85d223d01ee8..8df913a4ca63 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -602,14 +602,15 @@ public final class UsageStatsManager { /** * Query for events with specific UsageEventsQuery object. + * * <em>Note: if the user's device is not in an unlocked state (as defined by * {@link UserManager#isUserUnlocked()}), then {@code null} will be returned.</em> * * @param query The query object used to specify the query parameters. - * @return A {@link UsageEvents}. + * @return A {@link UsageEvents} which contains the events matching the query parameters. */ @FlaggedApi(Flags.FLAG_FILTER_BASED_EVENT_QUERY_API) - @NonNull + @Nullable @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public UsageEvents queryEvents(@NonNull UsageEventsQuery query) { try { diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 879656a89233..672e343959cb 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -76,6 +76,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -909,34 +910,20 @@ public final class CompanionDeviceManager { } /** - * Listener for any changes to the list of attached transports. - * - * @see com.android.server.companion.transport.Transport - * - * @hide - */ - public interface OnTransportsChangedListener { - /** - * Invoked when a transport is attached or detached. - * - * @param associations all the associations which have connected transports. - */ - void onTransportsChanged(@NonNull List<AssociationInfo> associations); - } - - /** * Adds a listener for any changes to the list of attached transports. - * {@link OnTransportsChangedListener#onTransportsChanged(List)} will be triggered with a list - * of existing transports when a transport is detached or a new transport is attached. + * Registered listener will be triggered with a list of existing transports when a transport + * is detached or a new transport is attached. * + * @param executor The executor which will be used to invoke the listener. + * @param listener Called when a transport is attached or detached. Contains the updated list of + * associations which have connected transports. * @see com.android.server.companion.transport.Transport - * * @hide */ @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void addOnTransportsChangedListener( @NonNull @CallbackExecutor Executor executor, - @NonNull OnTransportsChangedListener listener) { + @NonNull Consumer<List<AssociationInfo>> listener) { final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy( executor, listener); try { @@ -955,7 +942,7 @@ public final class CompanionDeviceManager { */ @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void removeOnTransportsChangedListener( - @NonNull OnTransportsChangedListener listener) { + @NonNull Consumer<List<AssociationInfo>> listener) { final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy( null, listener); try { @@ -983,28 +970,18 @@ public final class CompanionDeviceManager { } /** - * Listener that triggers a callback when a message is received through a connected transport. - * - * @see #addOnMessageReceivedListener(Executor, int, OnMessageReceivedListener) - * - * @hide - */ - public interface OnMessageReceivedListener { - /** - * Called when a message is received. - */ - void onMessageReceived(int associationId, @NonNull byte[] data); - } - - /** - * Adds a listener to trigger callbacks when messages of given type are received. + * Adds a listener that triggers when messages of given type are received. * + * @param executor The executor which will be used to invoke the listener. + * @param messageType Message type to be subscribed to. + * @param listener Called when a message is received. Contains the association ID of the message + * sender and the message payload as a byte array. * @hide */ @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void addOnMessageReceivedListener( @NonNull @CallbackExecutor Executor executor, int messageType, - @NonNull OnMessageReceivedListener listener) { + @NonNull BiConsumer<Integer, byte[]> listener) { final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy( executor, listener); try { @@ -1021,7 +998,7 @@ public final class CompanionDeviceManager { */ @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS) public void removeOnMessageReceivedListener(int messageType, - @NonNull OnMessageReceivedListener listener) { + @NonNull BiConsumer<Integer, byte[]> listener) { final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy( null, listener); try { @@ -1596,34 +1573,34 @@ public final class CompanionDeviceManager { private static class OnTransportsChangedListenerProxy extends IOnTransportsChangedListener.Stub { private final Executor mExecutor; - private final OnTransportsChangedListener mListener; + private final Consumer<List<AssociationInfo>> mListener; private OnTransportsChangedListenerProxy(Executor executor, - OnTransportsChangedListener listener) { + Consumer<List<AssociationInfo>> listener) { mExecutor = executor; mListener = listener; } @Override public void onTransportsChanged(@NonNull List<AssociationInfo> associations) { - mExecutor.execute(() -> mListener.onTransportsChanged(associations)); + mExecutor.execute(() -> mListener.accept(associations)); } } private static class OnMessageReceivedListenerProxy extends IOnMessageReceivedListener.Stub { private final Executor mExecutor; - private final OnMessageReceivedListener mListener; + private final BiConsumer<Integer, byte[]> mListener; private OnMessageReceivedListenerProxy(Executor executor, - OnMessageReceivedListener listener) { + BiConsumer<Integer, byte[]> listener) { mExecutor = executor; mListener = listener; } @Override public void onMessageReceived(int associationId, byte[] data) { - mExecutor.execute(() -> mListener.onMessageReceived(associationId, data)); + mExecutor.execute(() -> mListener.accept(associationId, data)); } } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 4bc1237a55d1..249c0e434e78 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4242,6 +4242,7 @@ public abstract class Context { VIRTUALIZATION_SERVICE, GRAMMATICAL_INFLECTION_SERVICE, SECURITY_STATE_SERVICE, + //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE, }) @Retention(RetentionPolicy.SOURCE) @@ -6527,6 +6528,18 @@ public abstract class Context { public static final String SECURITY_STATE_SERVICE = "security_state"; /** + * Use with {@link #getSystemService(String)} to retrieve an + * {@link android.app.ecm.EnhancedConfirmationManager}. + * + * @see #getSystemService(String) + * @see android.app.ecm.EnhancedConfirmationManager + * @hide + */ + @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED) + @SystemApi + public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 32ecb58ce241..1f25fd039dd8 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -80,7 +80,7 @@ interface IPackageInstaller { long timeout); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES,android.Manifest.permission.REQUEST_DELETE_PACKAGES})") - void requestArchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle, int flags); + void requestArchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})") void requestUnarchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 433226413917..f0efed97d8ed 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2352,7 +2352,6 @@ public class PackageInstaller { * communicated. * * @param statusReceiver Callback used to notify when the operation is completed. - * @param flags Flags for archiving. Can be 0 or {@link PackageManager#DELETE_SHOW_DIALOG}. * @throws PackageManager.NameNotFoundException If {@code packageName} isn't found or not * available to the caller or isn't archived. */ @@ -2360,12 +2359,11 @@ public class PackageInstaller { Manifest.permission.DELETE_PACKAGES, Manifest.permission.REQUEST_DELETE_PACKAGES}) @FlaggedApi(Flags.FLAG_ARCHIVING) - public void requestArchive(@NonNull String packageName, @NonNull IntentSender statusReceiver, - @DeleteFlags int flags) + public void requestArchive(@NonNull String packageName, @NonNull IntentSender statusReceiver) throws PackageManager.NameNotFoundException { try { mInstaller.requestArchive(packageName, mInstallerPackageName, statusReceiver, - new UserHandle(mUserId), flags); + new UserHandle(mUserId)); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); } catch (RemoteException e) { diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 8e5e8250c85d..aabbe698703c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2596,7 +2596,6 @@ public abstract class PackageManager { DELETE_SYSTEM_APP, DELETE_DONT_KILL_APP, DELETE_CHATTY, - DELETE_SHOW_DIALOG, }) @Retention(RetentionPolicy.SOURCE) public @interface DeleteFlags {} @@ -2649,12 +2648,6 @@ public abstract class PackageManager { public static final int DELETE_ARCHIVE = 0x00000010; /** - * Show a confirmation dialog to the user when app is being deleted. - */ - @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING) - public static final int DELETE_SHOW_DIALOG = 0x00000020; - - /** * Flag parameter for {@link #deletePackage} to indicate that package deletion * should be chatty. * diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 57749d43eb37..269c6c27821c 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -123,6 +123,7 @@ public final class UserProperties implements Parcelable { * @hide */ @IntDef(prefix = "SHOW_IN_LAUNCHER_", value = { + SHOW_IN_LAUNCHER_UNKNOWN, SHOW_IN_LAUNCHER_WITH_PARENT, SHOW_IN_LAUNCHER_SEPARATE, SHOW_IN_LAUNCHER_NO, @@ -131,6 +132,13 @@ public final class UserProperties implements Parcelable { public @interface ShowInLauncher { } /** + * Indicates that the show in launcher value for this profile is unknown or unsupported. + * @hide + */ + @TestApi + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_LAUNCHER_UNKNOWN = -1; + /** * Suggests that the launcher should show this user's apps in the main tab. * That is, either this user is a full user, so its apps should be presented accordingly, or, if * this user is a profile, then its apps should be shown alongside its parent's apps. @@ -157,6 +165,7 @@ public final class UserProperties implements Parcelable { * @hide */ @IntDef(prefix = "SHOW_IN_SETTINGS_", value = { + SHOW_IN_SETTINGS_UNKNOWN, SHOW_IN_SETTINGS_WITH_PARENT, SHOW_IN_SETTINGS_SEPARATE, SHOW_IN_SETTINGS_NO, @@ -165,6 +174,12 @@ public final class UserProperties implements Parcelable { public @interface ShowInSettings { } /** + * Indicates that the show in settings value for this profile is unknown or unsupported. + * @hide + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SETTINGS_UNKNOWN = -1; + /** * Suggests that the Settings app should show this user's apps in the main tab. * That is, either this user is a full user, so its apps should be presented accordingly, or, if * this user is a profile, then its apps should be shown alongside its parent's apps. @@ -309,6 +324,7 @@ public final class UserProperties implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "SHOW_IN_QUIET_MODE_", value = { + SHOW_IN_QUIET_MODE_UNKNOWN, SHOW_IN_QUIET_MODE_PAUSED, SHOW_IN_QUIET_MODE_HIDDEN, SHOW_IN_QUIET_MODE_DEFAULT, @@ -318,6 +334,12 @@ public final class UserProperties implements Parcelable { } /** + * Indicates that the show in quiet mode value for this profile is unknown. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_QUIET_MODE_UNKNOWN = -1; + + /** * Indicates that the profile should still be visible in quiet mode but should be shown as * paused (e.g. by greying out its icons). */ @@ -347,6 +369,7 @@ public final class UserProperties implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "SHOW_IN_SHARING_SURFACES_", value = { + SHOW_IN_SHARING_SURFACES_UNKNOWN, SHOW_IN_SHARING_SURFACES_SEPARATE, SHOW_IN_SHARING_SURFACES_WITH_PARENT, SHOW_IN_SHARING_SURFACES_NO, @@ -356,6 +379,12 @@ public final class UserProperties implements Parcelable { } /** + * Indicates that the show in launcher value for this profile is unknown or unsupported. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SHARING_SURFACES_UNKNOWN = SHOW_IN_LAUNCHER_UNKNOWN; + + /** * Indicates that the profile data and apps should be shown in sharing surfaces intermixed with * parent user's data and apps. */ @@ -379,7 +408,8 @@ public final class UserProperties implements Parcelable { * * @hide */ - @IntDef(prefix = {"CROSS_PROFILE_CONTENT_SHARING_STRATEGY_"}, value = { + @IntDef(prefix = {"CROSS_PROFILE_CONTENT_SHARING_"}, value = { + CROSS_PROFILE_CONTENT_SHARING_UNKNOWN, CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION, CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT }) @@ -388,6 +418,13 @@ public final class UserProperties implements Parcelable { } /** + * Signifies that cross-profile content sharing strategy, both to and from this profile, is + * unknown/unsupported. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; + + /** * Signifies that cross-profile content sharing strategy, both to and from this profile, should * not be delegated to any other user/profile. * For ex: diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index c7797c719e2c..7c5d3054c945 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -70,4 +70,11 @@ flag { namespace: "profile_experiences" description: "Add support for Private Space in resolver sheet" bug: "307515485" +} + +flag { + name: "move_quiet_mode_operations_to_separate_thread" + namespace: "profile_experiences" + description: "Move the quiet mode operations, happening on a background thread today, to a separate thread." + bug: "320483504" }
\ No newline at end of file diff --git a/core/java/android/content/res/FontScaleConverter.java b/core/java/android/content/res/FontScaleConverter.java index 088949e7eec2..f4312a905110 100644 --- a/core/java/android/content/res/FontScaleConverter.java +++ b/core/java/android/content/res/FontScaleConverter.java @@ -17,7 +17,9 @@ package android.content.res; +import android.annotation.AnyThread; import android.annotation.FlaggedApi; +import android.annotation.Nullable; /** * A converter for non-linear font scaling. Converts font sizes given in "sp" dimensions to a @@ -40,4 +42,35 @@ public interface FontScaleConverter { * Converts a dimension in "dp" back to "sp". */ float convertDpToSp(float dp); + + /** + * Returns true if non-linear font scaling curves would be in effect for the given scale, false + * if the scaling would follow a linear curve or for no scaling. + * + * <p>Example usage: {@code + * isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)} + */ + @AnyThread + static boolean isNonLinearFontScalingActive(float fontScale) { + return FontScaleConverterFactory.isNonLinearFontScalingActive(fontScale); + } + + /** + * Finds a matching FontScaleConverter for the given fontScale factor. + * + * Generally you shouldn't need this; you can use {@link + * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do + * the scaling conversion for you. Dimens and resources loaded from XML will also be + * automatically converted. But for UI frameworks or other situations where you need to do the + * conversion without an Android Context, you can use this method. + * + * @param fontScale the scale factor, usually from {@link Configuration#fontScale}. + * + * @return a converter for the given scale, or null if non-linear scaling should not be used. + */ + @Nullable + @AnyThread + static FontScaleConverter forScale(float fontScale) { + return FontScaleConverterFactory.forScale(fontScale); + } } diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java index 5d31cc0f0243..cbe4c62d7069 100644 --- a/core/java/android/content/res/FontScaleConverterFactory.java +++ b/core/java/android/content/res/FontScaleConverterFactory.java @@ -17,7 +17,6 @@ package android.content.res; import android.annotation.AnyThread; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.MathUtils; @@ -32,8 +31,9 @@ import com.android.internal.annotations.VisibleForTesting; * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do the * scaling conversion for you. But for UI frameworks or other situations where you need to do the * conversion without an Android Context, you can use this class. + * + * @hide */ -@FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) public class FontScaleConverterFactory { private static final float SCALE_KEY_MULTIPLIER = 100f; @@ -124,7 +124,6 @@ public class FontScaleConverterFactory { * <p>Example usage: * <code>isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code> */ - @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) @AnyThread public static boolean isNonLinearFontScalingActive(float fontScale) { return fontScale >= sMinScaleBeforeCurvesApplied; @@ -137,7 +136,6 @@ public class FontScaleConverterFactory { * * @return a converter for the given scale, or null if non-linear scaling should not be used. */ - @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) @Nullable @AnyThread public static FontScaleConverter forScale(float fontScale) { diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index f876eebe64c1..90cd4718fcee 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -33,4 +33,18 @@ flag { name: "new_settings_ui" description: "Enables new settings UI for VIC" bug: "315209085" +} + +flag { + namespace: "credential_manager" + name: "selector_ui_improvements_enabled" + description: "Enables Credential Selector UI improvements for VIC" + bug: "319448437" +} + +flag { + namespace: "credential_manager" + name: "configurable_selector_ui_enabled" + description: "Enables OEM configurable Credential Selector UI" + bug: "319448437" }
\ No newline at end of file diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index a0f4d8df0bd5..c0424dbeb813 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -16,6 +16,7 @@ package android.hardware.biometrics; +import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -25,6 +26,7 @@ import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT; import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import android.annotation.CallbackExecutor; +import android.annotation.DrawableRes; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -33,6 +35,7 @@ import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.content.Context; import android.content.DialogInterface; +import android.graphics.Bitmap; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Binder; @@ -160,6 +163,45 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Optional: Sets the drawable resource of the logo that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's icon will be used by default. Setting the logo is intended for large + * bundled applications that perform a wide range of functions and need to show distinct + * icons for each function. + * + * @param logoRes A drawable resource of the logo that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @NonNull + public BiometricPrompt.Builder setLogo(@DrawableRes int logoRes) { + mPromptInfo.setLogoRes(logoRes); + return this; + } + + /** + * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's icon will be used by default. Setting the logo is intended for large + * bundled applications that perform a wide range of functions and need to show distinct + * icons for each function. + * + * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @NonNull + public BiometricPrompt.Builder setLogo(@NonNull Bitmap logoBitmap) { + mPromptInfo.setLogoBitmap(logoBitmap); + return this; + } + + + /** * Required: Sets the title that will be shown on the prompt. * @param title The title to display. * @return This builder. @@ -676,6 +718,34 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Gets the drawable resource of the logo for the prompt, as set by + * {@link Builder#setLogo(int)}. Currently for system applications use only. + * + * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @DrawableRes + public int getLogoRes() { + return mPromptInfo.getLogoRes(); + } + + /** + * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogo(Bitmap)}. Currently for + * system applications use only. + * + * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @Nullable + public Bitmap getLogoBitmap() { + return mPromptInfo.getLogoBitmap(); + } + + + + /** * Gets the title for the prompt, as set by {@link Builder#setTitle(CharSequence)}. * @return The title of the prompt, which is guaranteed to be non-null. */ diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/core/java/android/hardware/biometrics/PromptContentItem.java index fa3783d6d874..c47b37aca2ea 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItem.java +++ b/core/java/android/hardware/biometrics/PromptContentItem.java @@ -21,9 +21,9 @@ import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import android.annotation.FlaggedApi; /** - * A list item shown on {@link PromptVerticalListContentView}. + * An item shown on {@link PromptContentView}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -public interface PromptContentListItem { +public interface PromptContentItem { } diff --git a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java index c31f8a63bcb8..c5e5a8076747 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java @@ -27,7 +27,7 @@ import android.os.Parcelable; * A list item with bulleted text shown on {@link PromptVerticalListContentView}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -public final class PromptContentListItemBulletedText implements PromptContentListItemParcelable { +public final class PromptContentItemBulletedText implements PromptContentItemParcelable { private final CharSequence mText; /** @@ -35,7 +35,7 @@ public final class PromptContentListItemBulletedText implements PromptContentLis * * @param text The text of this list item. */ - public PromptContentListItemBulletedText(@NonNull CharSequence text) { + public PromptContentItemBulletedText(@NonNull CharSequence text) { mText = text; } @@ -67,15 +67,15 @@ public final class PromptContentListItemBulletedText implements PromptContentLis * @see Parcelable.Creator */ @NonNull - public static final Creator<PromptContentListItemBulletedText> CREATOR = new Creator<>() { + public static final Creator<PromptContentItemBulletedText> CREATOR = new Creator<>() { @Override - public PromptContentListItemBulletedText createFromParcel(Parcel in) { - return new PromptContentListItemBulletedText(in.readCharSequence()); + public PromptContentItemBulletedText createFromParcel(Parcel in) { + return new PromptContentItemBulletedText(in.readCharSequence()); } @Override - public PromptContentListItemBulletedText[] newArray(int size) { - return new PromptContentListItemBulletedText[size]; + public PromptContentItemBulletedText[] newArray(int size) { + return new PromptContentItemBulletedText[size]; } }; } diff --git a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java index 15271f003c50..668912cfdf24 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java +++ b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java @@ -22,9 +22,9 @@ import android.annotation.FlaggedApi; import android.os.Parcelable; /** - * A parcelable {@link PromptContentListItem}. + * A parcelable {@link PromptContentItem}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -sealed interface PromptContentListItemParcelable extends PromptContentListItem, Parcelable - permits PromptContentListItemPlainText, PromptContentListItemBulletedText { +sealed interface PromptContentItemParcelable extends PromptContentItem, Parcelable + permits PromptContentItemPlainText, PromptContentItemBulletedText { } diff --git a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java index d72a7586d6ad..6434c5975c12 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java @@ -27,7 +27,7 @@ import android.os.Parcelable; * A list item with plain text shown on {@link PromptVerticalListContentView}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -public final class PromptContentListItemPlainText implements PromptContentListItemParcelable { +public final class PromptContentItemPlainText implements PromptContentItemParcelable { private final CharSequence mText; /** @@ -35,7 +35,7 @@ public final class PromptContentListItemPlainText implements PromptContentListIt * * @param text The text of this list item. */ - public PromptContentListItemPlainText(@NonNull CharSequence text) { + public PromptContentItemPlainText(@NonNull CharSequence text) { mText = text; } @@ -67,15 +67,15 @@ public final class PromptContentListItemPlainText implements PromptContentListIt * @see Parcelable.Creator */ @NonNull - public static final Creator<PromptContentListItemPlainText> CREATOR = new Creator<>() { + public static final Creator<PromptContentItemPlainText> CREATOR = new Creator<>() { @Override - public PromptContentListItemPlainText createFromParcel(Parcel in) { - return new PromptContentListItemPlainText(in.readCharSequence()); + public PromptContentItemPlainText createFromParcel(Parcel in) { + return new PromptContentItemPlainText(in.readCharSequence()); } @Override - public PromptContentListItemPlainText[] newArray(int size) { - return new PromptContentListItemPlainText[size]; + public PromptContentItemPlainText[] newArray(int size) { + return new PromptContentItemPlainText[size]; } }; } diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index c73ebd4dbe76..d788b37c781d 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -16,8 +16,10 @@ package android.hardware.biometrics; +import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; @@ -30,6 +32,8 @@ import java.util.List; */ public class PromptInfo implements Parcelable { + @DrawableRes private int mLogoRes = -1; + @Nullable private Bitmap mLogoBitmap; @NonNull private CharSequence mTitle; private boolean mUseDefaultTitle; @Nullable private CharSequence mSubtitle; @@ -56,6 +60,8 @@ public class PromptInfo implements Parcelable { } PromptInfo(Parcel in) { + mLogoRes = in.readInt(); + mLogoBitmap = in.readTypedObject(Bitmap.CREATOR); mTitle = in.readCharSequence(); mUseDefaultTitle = in.readBoolean(); mSubtitle = in.readCharSequence(); @@ -98,6 +104,8 @@ public class PromptInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLogoRes); + dest.writeTypedObject(mLogoBitmap, 0); dest.writeCharSequence(mTitle); dest.writeBoolean(mUseDefaultTitle); dest.writeCharSequence(mSubtitle); @@ -156,9 +164,30 @@ public class PromptInfo implements Parcelable { } return false; } + + /** + * Returns whether MANAGE_BIOMETRIC_DIALOG is contained. + */ + public boolean containsManageBioApiConfigurations() { + if (mLogoRes != -1) { + return true; + } else if (mLogoBitmap != null) { + return true; + } + return false; + } // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java) // Setters + public void setLogoRes(@DrawableRes int logoRes) { + mLogoRes = logoRes; + checkOnlyOneLogoSet(); + } + + public void setLogoBitmap(@NonNull Bitmap logoBitmap) { + mLogoBitmap = logoBitmap; + checkOnlyOneLogoSet(); + } public void setTitle(CharSequence title) { mTitle = title; @@ -244,6 +273,14 @@ public class PromptInfo implements Parcelable { } // Getters + @DrawableRes + public int getLogoRes() { + return mLogoRes; + } + + public Bitmap getLogoBitmap() { + return mLogoBitmap; + } public CharSequence getTitle() { return mTitle; @@ -337,4 +374,11 @@ public class PromptInfo implements Parcelable { public boolean isShowEmergencyCallButton() { return mShowEmergencyCallButton; } + + private void checkOnlyOneLogoSet() { + if (mLogoRes != -1 && mLogoBitmap != null) { + throw new IllegalStateException( + "Exclusively one of logo resource or logo bitmap can be set"); + } + } } diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java index f3cb189a5df5..f3e62907d845 100644 --- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java +++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java @@ -40,9 +40,9 @@ import java.util.List; * .setSubTitle(...) * .setContentView(new PromptVerticalListContentView.Builder() * .setDescription("test description") - * .addListItem(new PromptContentListItemPlainText("test item 1")) - * .addListItem(new PromptContentListItemPlainText("test item 2")) - * .addListItem(new PromptContentListItemBulletedText("test item 3")) + * .addListItem(new PromptContentItemPlainText("test item 1")) + * .addListItem(new PromptContentItemPlainText("test item 2")) + * .addListItem(new PromptContentItemBulletedText("test item 3")) * .build()) * .build(); * </pre> @@ -51,11 +51,11 @@ import java.util.List; public final class PromptVerticalListContentView implements PromptContentViewParcelable { private static final int MAX_ITEM_NUMBER = 20; private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; - private final List<PromptContentListItemParcelable> mContentList; + private final List<PromptContentItemParcelable> mContentList; private final CharSequence mDescription; private PromptVerticalListContentView( - @NonNull List<PromptContentListItemParcelable> contentList, + @NonNull List<PromptContentItemParcelable> contentList, @NonNull CharSequence description) { mContentList = contentList; mDescription = description; @@ -63,8 +63,8 @@ public final class PromptVerticalListContentView implements PromptContentViewPar private PromptVerticalListContentView(Parcel in) { mContentList = in.readArrayList( - PromptContentListItemParcelable.class.getClassLoader(), - PromptContentListItemParcelable.class); + PromptContentItemParcelable.class.getClassLoader(), + PromptContentItemParcelable.class); mDescription = in.readCharSequence(); } @@ -94,13 +94,13 @@ public final class PromptVerticalListContentView implements PromptContentViewPar } /** - * Gets the list of ListItem on the content view, as set by - * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentListItem)}. + * Gets the list of items on the content view, as set by + * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}. * * @return The item list on the content view. */ @NonNull - public List<PromptContentListItem> getListItems() { + public List<PromptContentItem> getListItems() { return new ArrayList<>(mContentList); } @@ -142,7 +142,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar * A builder that collects arguments to be shown on the vertical list view. */ public static final class Builder { - private final List<PromptContentListItemParcelable> mContentList = new ArrayList<>(); + private final List<PromptContentItemParcelable> mContentList = new ArrayList<>(); private CharSequence mDescription; /** @@ -159,28 +159,50 @@ public final class PromptVerticalListContentView implements PromptContentViewPar /** * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in - * total. + * total. The maximum length for each item is {@value MAX_EACH_ITEM_CHARACTER_NUMBER} + * characters. * * @param listItem The list item view to display * @return This builder. */ @NonNull - public Builder addListItem(@NonNull PromptContentListItem listItem) { + public Builder addListItem(@NonNull PromptContentItem listItem) { if (doesListItemExceedsCharLimit(listItem)) { throw new IllegalStateException( "The character number of list item exceeds " + MAX_EACH_ITEM_CHARACTER_NUMBER); } - mContentList.add((PromptContentListItemParcelable) listItem); + mContentList.add((PromptContentItemParcelable) listItem); return this; } - private boolean doesListItemExceedsCharLimit(PromptContentListItem listItem) { - if (listItem instanceof PromptContentListItemPlainText) { - return ((PromptContentListItemPlainText) listItem).getText().length() + + /** + * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in + * total. The maximum length for each item is {@value MAX_EACH_ITEM_CHARACTER_NUMBER} + * characters. + * + * @param listItem The list item view to display + * @param index The position at which to add the item + * @return This builder. + */ + @NonNull + public Builder addListItem(@NonNull PromptContentItem listItem, int index) { + if (doesListItemExceedsCharLimit(listItem)) { + throw new IllegalStateException( + "The character number of list item exceeds " + + MAX_EACH_ITEM_CHARACTER_NUMBER); + } + mContentList.add(index, (PromptContentItemParcelable) listItem); + return this; + } + + private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) { + if (listItem instanceof PromptContentItemPlainText) { + return ((PromptContentItemPlainText) listItem).getText().length() > MAX_EACH_ITEM_CHARACTER_NUMBER; - } else if (listItem instanceof PromptContentListItemBulletedText) { - return ((PromptContentListItemBulletedText) listItem).getText().length() + } else if (listItem instanceof PromptContentItemBulletedText) { + return ((PromptContentItemBulletedText) listItem).getText().length() > MAX_EACH_ITEM_CHARACTER_NUMBER; } else { return false; diff --git a/core/java/android/hardware/radio/TEST_MAPPING b/core/java/android/hardware/radio/TEST_MAPPING new file mode 100644 index 000000000000..ee4eeb634c84 --- /dev/null +++ b/core/java/android/hardware/radio/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "imports": [ + { + "path": "frameworks/base/core/tests/BroadcastRadioTests" + } + ] +} diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index 25fba60b9bb5..b9bb059bef42 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -17,9 +17,11 @@ package android.os; import static android.os.Flags.FLAG_STATE_OF_HEALTH_PUBLIC; +import static android.os.Flags.FLAG_BATTERY_PART_STATUS_API; import android.Manifest.permission; import android.annotation.FlaggedApi; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; @@ -236,6 +238,31 @@ public class BatteryManager { public static final int CHARGING_POLICY_ADAPTIVE_LONGLIFE = OsProtoEnums.CHARGING_POLICY_ADAPTIVE_LONGLIFE; // = 4 + // values for "battery part status" property + /** + * Battery part status is not supported. + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int PART_STATUS_UNSUPPORTED = 0; + + /** + * Battery is the original device battery. + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int PART_STATUS_ORIGINAL = 1; + + /** + * Battery has been replaced. + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int PART_STATUS_REPLACED = 2; + /** @hide */ @SuppressLint("UnflaggedApi") // TestApi without associated feature. @TestApi @@ -366,6 +393,32 @@ public class BatteryManager { @FlaggedApi(FLAG_STATE_OF_HEALTH_PUBLIC) public static final int BATTERY_PROPERTY_STATE_OF_HEALTH = 10; + /** + * Battery part serial number. + * + * <p class="note"> + * The sender must hold the {@link android.Manifest.permission#BATTERY_STATS} permission. + * + * @hide + */ + @RequiresPermission(permission.BATTERY_STATS) + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int BATTERY_PROPERTY_SERIAL_NUMBER = 11; + + /** + * Battery part status from a BATTERY_PART_STATUS_* value. + * + * <p class="note"> + * The sender must hold the {@link android.Manifest.permission#BATTERY_STATS} permission. + * + * @hide + */ + @RequiresPermission(permission.BATTERY_STATS) + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int BATTERY_PROPERTY_PART_STATUS = 12; + private final Context mContext; private final IBatteryStats mBatteryStats; private final IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; @@ -431,6 +484,25 @@ public class BatteryManager { } /** + * Same as queryProperty, but for strings. + */ + private String queryStringProperty(int id) { + if (mBatteryPropertiesRegistrar == null) { + return null; + } + + try { + BatteryProperty prop = new BatteryProperty(); + if (mBatteryPropertiesRegistrar.getProperty(id, prop) == 0) { + return prop.getString(); + } + return null; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Return the value of a battery property of integer type. * * @param id identifier of the requested property @@ -464,6 +536,21 @@ public class BatteryManager { } /** + * Return the value of a battery property of String type. If the + * platform does not provide the property queried, this value will + * be null. + * + * @param id identifier of the requested property. + * + * @return the property value, or null if not supported. + */ + @Nullable + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public String getStringProperty(int id) { + return queryStringProperty(id); + } + + /** * Return true if the plugType given is wired * @param plugType {@link #BATTERY_PLUGGED_AC}, {@link #BATTERY_PLUGGED_USB}, * or {@link #BATTERY_PLUGGED_WIRELESS} diff --git a/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java index b40988a938bc..464577f58241 100644 --- a/core/java/android/os/BatteryProperty.java +++ b/core/java/android/os/BatteryProperty.java @@ -28,12 +28,14 @@ import android.os.Parcelable; */ public class BatteryProperty implements Parcelable { private long mValueLong; + private String mValueString; /** * @hide */ public BatteryProperty() { mValueLong = Long.MIN_VALUE; + mValueString = null; } /** @@ -46,14 +48,23 @@ public class BatteryProperty implements Parcelable { /** * @hide */ + public String getString() { + return mValueString; + } + + /** + * @hide + */ public void setLong(long val) { mValueLong = val; } - /* - * Parcel read/write code must be kept in sync with - * frameworks/native/services/batteryservice/BatteryProperty.cpp + /** + * @hide */ + public void setString(String val) { + mValueString = val; + } private BatteryProperty(Parcel p) { readFromParcel(p); @@ -61,10 +72,12 @@ public class BatteryProperty implements Parcelable { public void readFromParcel(Parcel p) { mValueLong = p.readLong(); + mValueString = p.readString8(); } public void writeToParcel(Parcel p, int flags) { p.writeLong(mValueLong); + p.writeString8(mValueString); } public static final @android.annotation.NonNull Parcelable.Creator<BatteryProperty> CREATOR diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 0db90bff48fd..82518bfbfd8d 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -106,3 +106,11 @@ flag { bug: "305311707" is_fixed_read_only: true } + +flag { + name: "battery_part_status_api" + namespace: "phoenix" + description: "Feature flag for adding Health HAL v3 APIs." + is_fixed_read_only: true + bug: "309792384" +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index bced21730a34..39b6aeb814f6 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -45,7 +45,8 @@ flag { } flag { - name: "enhanced_confirmation_mode_apis" + name: "enhanced_confirmation_mode_apis_enabled" + is_fixed_read_only: true namespace: "permissions" description: "enable enhanced confirmation mode apis" bug: "310220212" diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 7d84bb390853..58159c2b5693 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3176,15 +3176,7 @@ public final class Settings { } public void destroy() { - try { - // If this process is the system server process, mArray is the same object as - // the memory int array kept inside SettingsProvider, so skipping the close() - if (!Settings.isInSystemServer() && !mArray.isClosed()) { - mArray.close(); - } - } catch (IOException e) { - Log.e(TAG, "Error closing backing array", e); - } + maybeCloseGenerationArray(mArray); } @Override @@ -3197,6 +3189,21 @@ public final class Settings { } } + private static void maybeCloseGenerationArray(@Nullable MemoryIntArray array) { + if (array == null) { + return; + } + try { + // If this process is the system server process, the MemoryIntArray received from Parcel + // is the same object as the one kept inside SettingsProvider, so skipping the close(). + if (!Settings.isInSystemServer() && !array.isClosed()) { + array.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error closing the generation tracking array", e); + } + } + private static final class ContentProviderHolder { private final Object mLock = new Object(); @@ -3496,6 +3503,8 @@ public final class Settings { mGenerationTrackers.put(name, new GenerationTracker(name, array, index, generation, mGenerationTrackerErrorHandler)); + } else { + maybeCloseGenerationArray(array); } } if (mGenerationTrackers.get(name) != null @@ -3733,6 +3742,8 @@ public final class Settings { new GenerationTracker(prefix, array, index, generation, mGenerationTrackerErrorHandler)); currentGeneration = generation; + } else { + maybeCloseGenerationArray(array); } } if (mGenerationTrackers.get(prefix) != null && currentGeneration @@ -7310,6 +7321,28 @@ public final class Settings { "bluetooth_le_broadcast_app_source_name"; /** + * This is used by LocalBluetoothLeBroadcast to downgrade the broadcast quality to improve + * compatibility. + * + * <ul> + * <li>0 = false + * <li>1 = true + * </ul> + * + * @hide + */ + public static final String BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY = + "bluetooth_le_broadcast_improve_compatibility"; + + /** + * This is used by LocalBluetoothLeBroadcast to store the fallback active device address. + * + * @hide + */ + public static final String BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS = + "bluetooth_le_broadcast_fallback_active_device_address"; + + /** * Ringtone routing value for hearing aid. It routes ringtone to hearing aid or device * speaker. * <ul> diff --git a/core/java/android/provider/TEST_MAPPING b/core/java/android/provider/TEST_MAPPING index 8b4a99e38299..d5ac7a7de461 100644 --- a/core/java/android/provider/TEST_MAPPING +++ b/core/java/android/provider/TEST_MAPPING @@ -28,5 +28,10 @@ } ] } + ], + "postsubmit": [ + { + "name": "CtsDeviceConfigTestCases" + } ] } diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 30524a1132fa..1994058441d5 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -53,7 +53,7 @@ flag { flag { name: "frp_enforcement" - namespace: "android_hw_security" + namespace: "hardware_backed_security" description: "This flag controls whether PDB enforces FRP" bug: "290312729" is_fixed_read_only: true diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java index 03ebae5c5199..90049e6a934a 100644 --- a/core/java/android/service/notification/ZenDeviceEffects.java +++ b/core/java/android/service/notification/ZenDeviceEffects.java @@ -20,7 +20,6 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.TestApi; import android.app.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -37,8 +36,8 @@ import java.util.Objects; @FlaggedApi(Flags.FLAG_MODES_API) public final class ZenDeviceEffects implements Parcelable { - /** Used to track which rule variables have been modified by the user. - * Should be checked against the bitmask {@link #getUserModifiedFields()}. + /** + * Enum for the user-modifiable fields in this object. * @hide */ @IntDef(flag = true, prefix = { "FIELD_" }, value = { @@ -59,52 +58,42 @@ public final class ZenDeviceEffects implements Parcelable { /** * @hide */ - @TestApi public static final int FIELD_GRAYSCALE = 1 << 0; /** * @hide */ - @TestApi public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 1 << 1; /** * @hide */ - @TestApi public static final int FIELD_DIM_WALLPAPER = 1 << 2; /** * @hide */ - @TestApi public static final int FIELD_NIGHT_MODE = 1 << 3; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 1 << 4; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_TAP_TO_WAKE = 1 << 5; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_TILT_TO_WAKE = 1 << 6; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_TOUCH = 1 << 7; /** * @hide */ - @TestApi public static final int FIELD_MINIMIZE_RADIO_USAGE = 1 << 8; /** * @hide */ - @TestApi public static final int FIELD_MAXIMIZE_DOZE = 1 << 9; private final boolean mGrayscale; @@ -119,13 +108,10 @@ public final class ZenDeviceEffects implements Parcelable { private final boolean mMinimizeRadioUsage; private final boolean mMaximizeDoze; - private final @ModifiableField int mUserModifiedFields; // Bitwise representation - private ZenDeviceEffects(boolean grayscale, boolean suppressAmbientDisplay, boolean dimWallpaper, boolean nightMode, boolean disableAutoBrightness, boolean disableTapToWake, boolean disableTiltToWake, boolean disableTouch, - boolean minimizeRadioUsage, boolean maximizeDoze, - @ModifiableField int userModifiedFields) { + boolean minimizeRadioUsage, boolean maximizeDoze) { mGrayscale = grayscale; mSuppressAmbientDisplay = suppressAmbientDisplay; mDimWallpaper = dimWallpaper; @@ -136,7 +122,6 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = disableTouch; mMinimizeRadioUsage = minimizeRadioUsage; mMaximizeDoze = maximizeDoze; - mUserModifiedFields = userModifiedFields; } @Override @@ -153,15 +138,14 @@ public final class ZenDeviceEffects implements Parcelable { && this.mDisableTiltToWake == that.mDisableTiltToWake && this.mDisableTouch == that.mDisableTouch && this.mMinimizeRadioUsage == that.mMinimizeRadioUsage - && this.mMaximizeDoze == that.mMaximizeDoze - && this.mUserModifiedFields == that.mUserModifiedFields; + && this.mMaximizeDoze == that.mMaximizeDoze; } @Override public int hashCode() { return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, - mMinimizeRadioUsage, mMaximizeDoze, mUserModifiedFields); + mMinimizeRadioUsage, mMaximizeDoze); } @Override @@ -177,11 +161,11 @@ public final class ZenDeviceEffects implements Parcelable { if (mDisableTouch) effects.add("disableTouch"); if (mMinimizeRadioUsage) effects.add("minimizeRadioUsage"); if (mMaximizeDoze) effects.add("maximizeDoze"); - return "[" + String.join(", ", effects) + "]" - + " userModifiedFields: " + modifiedFieldsToString(mUserModifiedFields); + return "[" + String.join(", ", effects) + "]"; } - private String modifiedFieldsToString(int bitmask) { + /** @hide */ + public static String fieldsToString(@ModifiableField int bitmask) { ArrayList<String> modified = new ArrayList<>(); if ((bitmask & FIELD_GRAYSCALE) != 0) { modified.add("FIELD_GRAYSCALE"); @@ -312,7 +296,7 @@ public final class ZenDeviceEffects implements Parcelable { return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), - in.readBoolean(), in.readInt()); + in.readBoolean()); } @Override @@ -321,16 +305,6 @@ public final class ZenDeviceEffects implements Parcelable { } }; - /** - * Gets the bitmask representing which fields are user modified. Bits are set using - * {@link ModifiableField}. - * @hide - */ - @TestApi - public @ModifiableField int getUserModifiedFields() { - return mUserModifiedFields; - } - @Override public int describeContents() { return 0; @@ -348,7 +322,6 @@ public final class ZenDeviceEffects implements Parcelable { dest.writeBoolean(mDisableTouch); dest.writeBoolean(mMinimizeRadioUsage); dest.writeBoolean(mMaximizeDoze); - dest.writeInt(mUserModifiedFields); } /** Builder class for {@link ZenDeviceEffects} objects. */ @@ -365,7 +338,6 @@ public final class ZenDeviceEffects implements Parcelable { private boolean mDisableTouch; private boolean mMinimizeRadioUsage; private boolean mMaximizeDoze; - private @ModifiableField int mUserModifiedFields; /** * Instantiates a new {@link ZenPolicy.Builder} with all effects set to default (disabled). @@ -388,7 +360,6 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = zenDeviceEffects.shouldDisableTouch(); mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage(); mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze(); - mUserModifiedFields = zenDeviceEffects.mUserModifiedFields; } /** @@ -510,24 +481,13 @@ public final class ZenDeviceEffects implements Parcelable { return this; } - /** - * Sets the bitmask representing which fields are user modified. See the FIELD_ constants. - * @hide - */ - @TestApi - @NonNull - public Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { - mUserModifiedFields = userModifiedFields; - return this; - } - /** Builds a {@link ZenDeviceEffects} object based on the builder's state. */ @NonNull public ZenDeviceEffects build() { return new ZenDeviceEffects(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage, - mMaximizeDoze, mUserModifiedFields); + mMaximizeDoze); } } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index d4d602da0071..c479877fe98e 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -206,7 +206,7 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_CONV = "convos"; private static final String ALLOW_ATT_CONV_FROM = "convosFrom"; private static final String ALLOW_ATT_CHANNELS = "priorityChannels"; - private static final String USER_MODIFIED_FIELDS = "policyUserModifiedFields"; + private static final String POLICY_USER_MODIFIED_FIELDS = "policyUserModifiedFields"; private static final String DISALLOW_TAG = "disallow"; private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects"; private static final String STATE_TAG = "state"; @@ -806,6 +806,9 @@ public class ZenModeConfig implements Parcelable { rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC); rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN); rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0); + rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); + rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, + DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); Long deletionInstant = tryParseLong( parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); if (deletionInstant != null) { @@ -858,6 +861,9 @@ public class ZenModeConfig implements Parcelable { } out.attributeInt(null, RULE_ATT_TYPE, rule.type); out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields); + out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); + out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, + rule.zenDeviceEffectsUserModifiedFields); if (rule.deletionInstant != null) { out.attributeLong(null, RULE_ATT_DELETION_INSTANT, rule.deletionInstant.toEpochMilli()); @@ -924,7 +930,6 @@ public class ZenModeConfig implements Parcelable { builder.allowPriorityChannels(channels == ZenPolicy.STATE_ALLOW); policySet = true; } - builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0)); } if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) { @@ -1037,7 +1042,6 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesApi()) { writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out); - out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields()); } } @@ -1083,7 +1087,6 @@ public class ZenModeConfig implements Parcelable { .setShouldMinimizeRadioUsage( safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false)) .setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false)) - .setUserModifiedFields(safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0)) .build(); return deviceEffects.hasEffects() ? deviceEffects : null; @@ -1108,8 +1111,6 @@ public class ZenModeConfig implements Parcelable { writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, deviceEffects.shouldMinimizeRadioUsage()); writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze()); - out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, - deviceEffects.getUserModifiedFields()); } private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value) @@ -2041,7 +2042,9 @@ public class ZenModeConfig implements Parcelable { public String triggerDescription; public String iconResName; public boolean allowManualInvocation; - public int userModifiedFields; + @AutomaticZenRule.ModifiableField public int userModifiedFields; + @ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields; + @ZenDeviceEffects.ModifiableField public int zenDeviceEffectsUserModifiedFields; @Nullable public Instant deletionInstant; // Only set on deleted rules. public ZenRule() { } @@ -2076,6 +2079,8 @@ public class ZenModeConfig implements Parcelable { triggerDescription = source.readString(); type = source.readInt(); userModifiedFields = source.readInt(); + zenPolicyUserModifiedFields = source.readInt(); + zenDeviceEffectsUserModifiedFields = source.readInt(); if (source.readInt() == 1) { deletionInstant = Instant.ofEpochMilli(source.readLong()); } @@ -2083,15 +2088,21 @@ public class ZenModeConfig implements Parcelable { } /** - * @see AutomaticZenRule#canUpdate() + * Whether this ZenRule can be updated by an app. In general, rules that have been + * customized by the user cannot be further updated by an app, with some exceptions: + * <ul> + * <li>Non user-configurable fields, like type, icon, configurationActivity, etc. + * <li>Name, if the name was not specifically modified by the user (to support language + * switches). + * </ul> */ @FlaggedApi(Flags.FLAG_MODES_API) public boolean canBeUpdatedByApp() { // The rule is considered updateable if its bitmask has no user modifications, and // the bitmasks of the policy and device effects have no modification. return userModifiedFields == 0 - && (zenPolicy == null || zenPolicy.getUserModifiedFields() == 0) - && (zenDeviceEffects == null || zenDeviceEffects.getUserModifiedFields() == 0); + && zenPolicyUserModifiedFields == 0 + && zenDeviceEffectsUserModifiedFields == 0; } @Override @@ -2139,6 +2150,8 @@ public class ZenModeConfig implements Parcelable { dest.writeString(triggerDescription); dest.writeInt(type); dest.writeInt(userModifiedFields); + dest.writeInt(zenPolicyUserModifiedFields); + dest.writeInt(zenDeviceEffectsUserModifiedFields); if (deletionInstant != null) { dest.writeInt(1); dest.writeLong(deletionInstant.toEpochMilli()); @@ -2173,8 +2186,20 @@ public class ZenModeConfig implements Parcelable { .append(",allowManualInvocation=").append(allowManualInvocation) .append(",iconResName=").append(iconResName) .append(",triggerDescription=").append(triggerDescription) - .append(",type=").append(type) - .append(",userModifiedFields=").append(userModifiedFields); + .append(",type=").append(type); + if (userModifiedFields != 0) { + sb.append(",userModifiedFields=") + .append(AutomaticZenRule.fieldsToString(userModifiedFields)); + } + if (zenPolicyUserModifiedFields != 0) { + sb.append(",zenPolicyUserModifiedFields=") + .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields)); + } + if (zenDeviceEffectsUserModifiedFields != 0) { + sb.append(",zenDeviceEffectsUserModifiedFields=") + .append(ZenDeviceEffects.fieldsToString( + zenDeviceEffectsUserModifiedFields)); + } if (deletionInstant != null) { sb.append(",deletionInstant=").append(deletionInstant); } @@ -2238,6 +2263,9 @@ public class ZenModeConfig implements Parcelable { && Objects.equals(other.triggerDescription, triggerDescription) && other.type == type && other.userModifiedFields == userModifiedFields + && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields + && other.zenDeviceEffectsUserModifiedFields + == zenDeviceEffectsUserModifiedFields && Objects.equals(other.deletionInstant, deletionInstant); } @@ -2250,7 +2278,8 @@ public class ZenModeConfig implements Parcelable { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, zenDeviceEffects, modified, allowManualInvocation, iconResName, - triggerDescription, type, userModifiedFields, deletionInstant); + triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields, + zenDeviceEffectsUserModifiedFields, deletionInstant); } return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, modified); diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index 2cda7c882cdb..fb491d010f54 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -45,8 +45,8 @@ import java.util.Objects; */ public final class ZenPolicy implements Parcelable { - /** Used to track which rule variables have been modified by the user. - * Should be checked against the bitmask {@link #getUserModifiedFields()}. + /** + * Enum for the user-modifiable fields in this object. * @hide */ @IntDef(flag = true, prefix = { "FIELD_" }, value = { @@ -76,7 +76,6 @@ public final class ZenPolicy implements Parcelable { * the same time. * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_MESSAGES = 1 << 0; /** @@ -84,7 +83,6 @@ public final class ZenPolicy implements Parcelable { * the same time. * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_CALLS = 1 << 1; /** @@ -92,13 +90,11 @@ public final class ZenPolicy implements Parcelable { * set at the same time. * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_CONVERSATIONS = 1 << 2; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_ALLOW_CHANNELS = 1 << 3; /** @@ -109,73 +105,61 @@ public final class ZenPolicy implements Parcelable { /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16; @@ -186,7 +170,6 @@ public final class ZenPolicy implements Parcelable { private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET; @FlaggedApi(Flags.FLAG_MODES_API) private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET; - private final @ModifiableField int mUserModifiedFields; // Bitwise representation /** @hide */ @IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = { @@ -388,22 +371,19 @@ public final class ZenPolicy implements Parcelable { public ZenPolicy() { mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0)); mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0)); - mUserModifiedFields = 0; } /** @hide */ @FlaggedApi(Flags.FLAG_MODES_API) public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects, @PeopleType int priorityMessages, @PeopleType int priorityCalls, - @ConversationSenders int conversationSenders, @ChannelType int allowChannels, - @ModifiableField int userModifiedFields) { + @ConversationSenders int conversationSenders, @ChannelType int allowChannels) { mPriorityCategories = priorityCategories; mVisualEffects = visualEffects; mPriorityMessages = priorityMessages; mPriorityCalls = priorityCalls; mConversationSenders = conversationSenders; mAllowChannels = allowChannels; - mUserModifiedFields = userModifiedFields; } /** @@ -633,8 +613,6 @@ public final class ZenPolicy implements Parcelable { * is not set, it is (@link STATE_UNSET} and will not change the current set policy. */ public static final class Builder { - private @ModifiableField int mUserModifiedFields; - private ZenPolicy mZenPolicy; public Builder() { @@ -649,9 +627,6 @@ public final class ZenPolicy implements Parcelable { public Builder(@Nullable ZenPolicy policy) { if (policy != null) { mZenPolicy = policy.copy(); - if (Flags.modesApi()) { - mUserModifiedFields = policy.mUserModifiedFields; - } } else { mZenPolicy = new ZenPolicy(); } @@ -662,11 +637,10 @@ public final class ZenPolicy implements Parcelable { */ public @NonNull ZenPolicy build() { if (Flags.modesApi()) { - return new ZenPolicy(new ArrayList<Integer>(mZenPolicy.mPriorityCategories), - new ArrayList<Integer>(mZenPolicy.mVisualEffects), + return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories), + new ArrayList<>(mZenPolicy.mVisualEffects), mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls, - mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels, - mUserModifiedFields); + mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels); } else { return mZenPolicy.copy(); } @@ -1025,28 +999,6 @@ public final class ZenPolicy implements Parcelable { mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE; return this; } - - /** - * Sets the user modified fields bitmask. - * @hide - */ - @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) - public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { - mUserModifiedFields = userModifiedFields; - return this; - } - } - - /** - Gets the bitmask representing which fields are user modified. Bits are set using - * {@link ModifiableField}. - * @hide - */ - @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) - public @ModifiableField int getUserModifiedFields() { - return mUserModifiedFields; } @Override @@ -1063,7 +1015,6 @@ public final class ZenPolicy implements Parcelable { dest.writeInt(mConversationSenders); if (Flags.modesApi()) { dest.writeInt(mAllowChannels); - dest.writeInt(mUserModifiedFields); } } @@ -1079,7 +1030,7 @@ public final class ZenPolicy implements Parcelable { trimList(source.readArrayList(Integer.class.getClassLoader(), Integer.class), NUM_VISUAL_EFFECTS), source.readInt(), source.readInt(), source.readInt(), - source.readInt(), source.readInt() + source.readInt() ); } else { policy = new ZenPolicy(); @@ -1114,14 +1065,12 @@ public final class ZenPolicy implements Parcelable { conversationTypeToString(mConversationSenders)); if (Flags.modesApi()) { sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels)); - sb.append(", userModifiedFields=") - .append(modifiedFieldsToString(mUserModifiedFields)); } return sb.append('}').toString(); } - @FlaggedApi(Flags.FLAG_MODES_API) - private String modifiedFieldsToString(@ModifiableField int bitmask) { + /** @hide */ + public static String fieldsToString(@ModifiableField int bitmask) { ArrayList<String> modified = new ArrayList<>(); if ((bitmask & FIELD_MESSAGES) != 0) { modified.add("FIELD_MESSAGES"); @@ -1332,8 +1281,7 @@ public final class ZenPolicy implements Parcelable { && other.mPriorityMessages == mPriorityMessages && other.mConversationSenders == mConversationSenders; if (Flags.modesApi()) { - return eq && other.mAllowChannels == mAllowChannels - && other.mUserModifiedFields == mUserModifiedFields; + return eq && other.mAllowChannels == mAllowChannels; } return eq; } @@ -1342,7 +1290,7 @@ public final class ZenPolicy implements Parcelable { public int hashCode() { if (Flags.modesApi()) { return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, - mPriorityMessages, mConversationSenders, mAllowChannels, mUserModifiedFields); + mPriorityMessages, mConversationSenders, mAllowChannels); } return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages, mConversationSenders); diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index adc54f5b5a8c..f2bdbf67e76e 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -325,7 +325,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryDetected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryDetected(partialQuery); + mExecutor.execute(()->mCallback.onQueryDetected(partialQuery)); } }); } @@ -335,7 +335,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryFinished"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryFinished(); + mExecutor.execute(()->mCallback.onQueryFinished()); } }); } @@ -345,7 +345,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryRejected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryRejected(); + mExecutor.execute(()->mCallback.onQueryRejected()); } }); } diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index cf1156db55e5..fb57921b1529 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -1681,6 +1681,10 @@ public class PhoneStateListener { @EmergencyCallbackModeStopReason int reason) { // not support. Can't override. Use TelephonyCallback. } + + public final void onSimultaneousCallingStateChanged(int[] subIds) { + // not supported on the deprecated interface - Use TelephonyCallback instead + } } private void log(String s) { diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index 19bcf28d6b83..dc6a035a8176 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -18,6 +18,7 @@ package android.telephony; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; @@ -33,15 +34,19 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.IPhoneStateListener; +import com.android.internal.telephony.flags.Flags; import dalvik.system.VMRuntime; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; +import java.util.stream.Collectors; /** * A callback class for monitoring changes in specific telephony states @@ -627,6 +632,18 @@ public class TelephonyCallback { public static final int EVENT_EMERGENCY_CALLBACK_MODE_CHANGED = 40; /** + * Event for listening to changes in simultaneous cellular calling subscriptions. + * + * @see SimultaneousCellularCallingSupportListener + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @SystemApi + public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; + + /** * @hide */ @IntDef(prefix = {"EVENT_"}, value = { @@ -669,7 +686,8 @@ public class TelephonyCallback { EVENT_LINK_CAPACITY_ESTIMATE_CHANGED, EVENT_TRIGGER_NOTIFY_ANBR, EVENT_MEDIA_QUALITY_STATUS_CHANGED, - EVENT_EMERGENCY_CALLBACK_MODE_CHANGED + EVENT_EMERGENCY_CALLBACK_MODE_CHANGED, + EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface TelephonyEvent { @@ -1373,6 +1391,44 @@ public class TelephonyCallback { } /** + * Interface for listening to changes in the simultaneous cellular calling state for active + * cellular subscriptions. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + @SystemApi + public interface SimultaneousCellularCallingSupportListener { + /** + * Notify the Listener that the subscriptions available for simultaneous <b>cellular</b> + * calling have changed. + * <p> + * If we have an ongoing <b>cellular</b> call on one subscription in this Set, a + * simultaneous incoming or outgoing <b>cellular</b> call is possible on any of the + * subscriptions in this Set. On a traditional Dual Sim Dual Standby device, simultaneous + * calling is not possible between subscriptions, where on a Dual Sim Dual Active device, + * simultaneous calling may be possible between subscriptions in certain network conditions. + * <p> + * Note: This listener only tracks the capability of the modem to perform simultaneous + * cellular calls and does not track the simultaneous calling state of scenarios based on + * multiple IMS registration over multiple transports (WiFi/Internet calling). + * <p> + * Note: This listener fires for all changes to cellular calling subscriptions independent + * of which subscription it is registered on. + * + * @param simultaneousCallingSubscriptionIds The Set of subscription IDs that support + * simultaneous calling. If there is an ongoing call on a subscription in this Set, then a + * simultaneous incoming or outgoing call is only possible for other subscriptions in this + * Set. If there is an ongoing call on a subscription that is not in this Set, then + * simultaneous calling is not possible at the current time. + * + */ + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + void onSimultaneousCellularCallingSubscriptionsChanged( + @NonNull Set<Integer> simultaneousCallingSubscriptionIds); + } + + /** * Interface for call attributes listener. * * @hide @@ -1976,6 +2032,17 @@ public class TelephonyCallback { allowedNetworkType))); } + public void onSimultaneousCallingStateChanged(int[] subIds) { + SimultaneousCellularCallingSupportListener listener = + (SimultaneousCellularCallingSupportListener) mTelephonyCallbackWeakRef.get(); + if (listener == null) return; + + Binder.withCleanCallingIdentity( + () -> mExecutor.execute( + () -> listener.onSimultaneousCellularCallingSubscriptionsChanged( + Arrays.stream(subIds).boxed().collect(Collectors.toSet())))); + } + public void onLinkCapacityEstimateChanged( List<LinkCapacityEstimate> linkCapacityEstimateList) { LinkCapacityEstimateChangedListener listener = diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 886727ea43ef..8935ab38451a 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -16,9 +16,11 @@ package android.telephony; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -50,6 +52,7 @@ import com.android.internal.telephony.ICarrierConfigChangeListener; import com.android.internal.telephony.ICarrierPrivilegesCallback; import com.android.internal.telephony.IOnSubscriptionsChangedListener; import com.android.internal.telephony.ITelephonyRegistry; +import com.android.server.telecom.flags.Flags; import java.lang.ref.WeakReference; import java.util.Arrays; @@ -66,11 +69,13 @@ import java.util.stream.Collectors; * or {@link PhoneCapability} changed. This might trigger callback from applications side through * {@link android.telephony.PhoneStateListener} * - * TODO: limit API access to only carrier apps with certain permissions or apps running on + * Limit API access to only carrier apps with certain permissions or apps running on * privileged UID. * * @hide */ +@SystemApi +@FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) public class TelephonyRegistryManager { private static final String TAG = "TelephonyRegistryManager"; @@ -120,6 +125,7 @@ public class TelephonyRegistryManager { * @param listener an instance of {@link SubscriptionManager.OnSubscriptionsChangedListener} * with onSubscriptionsChanged overridden. * @param executor the executor that will execute callbacks. + * @hide */ public void addOnSubscriptionsChangedListener( @NonNull SubscriptionManager.OnSubscriptionsChangedListener listener, @@ -155,6 +161,7 @@ public class TelephonyRegistryManager { * invoke the listener fails. * * @param listener that is to be unregistered. + * @hide */ public void removeOnSubscriptionsChangedListener( @NonNull SubscriptionManager.OnSubscriptionsChangedListener listener) { @@ -180,6 +187,7 @@ public class TelephonyRegistryManager { * {@link SubscriptionManager.OnOpportunisticSubscriptionsChangedListener} with * onOpportunisticSubscriptionsChanged overridden. * @param executor an Executor that will execute callbacks. + * @hide */ public void addOnOpportunisticSubscriptionsChangedListener( @NonNull SubscriptionManager.OnOpportunisticSubscriptionsChangedListener listener, @@ -221,6 +229,7 @@ public class TelephonyRegistryManager { * listener fails. * * @param listener that is to be unregistered. + * @hide */ public void removeOnOpportunisticSubscriptionsChangedListener( @NonNull SubscriptionManager.OnOpportunisticSubscriptionsChangedListener listener) { @@ -252,6 +261,7 @@ public class TelephonyRegistryManager { * @param listener Listener providing callback * @param events Events * @param notifyNow Whether to notify instantly + * @hide */ public void listenFromListener(int subId, @NonNull boolean renounceFineLocationAccess, @NonNull boolean renounceCoarseLocationAccess, @NonNull String pkg, @@ -318,6 +328,7 @@ public class TelephonyRegistryManager { * @param active Whether the carrier network change is or shortly will be * active. Set this value to true to begin showing alternative UI and false to stop. * @see TelephonyManager#hasCarrierPrivileges + * @hide */ public void notifyCarrierNetworkChange(boolean active) { try { @@ -344,6 +355,7 @@ public class TelephonyRegistryManager { * @param active whether the carrier network change is or shortly will be active. Set this value * to true to begin showing alternative UI and false to stop. * @see TelephonyManager#hasCarrierPrivileges + * @hide */ public void notifyCarrierNetworkChange(int subscriptionId, boolean active) { try { @@ -362,6 +374,7 @@ public class TelephonyRegistryManager { * @param subId for which call state changed. * @param state latest call state. e.g, offhook, ringing * @param incomingNumber incoming phone number. + * @hide */ public void notifyCallStateChanged(int slotIndex, int subId, @CallState int state, @Nullable String incomingNumber) { @@ -380,6 +393,7 @@ public class TelephonyRegistryManager { * @param incomingNumber incoming phone number. * @hide */ + @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyCallStateChangedForAllSubscriptions(@CallState int state, @Nullable String incomingNumber) { @@ -424,6 +438,7 @@ public class TelephonyRegistryManager { * subId is invalid. * @param subId for which the service state changed. * @param state service state e.g, in service, out of service or roaming status. + * @hide */ public void notifyServiceStateChanged(int slotIndex, int subId, @NonNull ServiceState state) { try { @@ -441,6 +456,7 @@ public class TelephonyRegistryManager { * subId is invalid. * @param subId for which the signalstrength changed. * @param signalStrength e.g, signalstrength level {@see SignalStrength#getLevel()} + * @hide */ public void notifySignalStrengthChanged(int slotIndex, int subId, @NonNull SignalStrength signalStrength) { @@ -461,6 +477,7 @@ public class TelephonyRegistryManager { * @param subId for which message waiting indicator changed. * @param msgWaitingInd {@code true} indicates there is message-waiting indicator, {@code false} * otherwise. + * @hide */ public void notifyMessageWaitingChanged(int slotIndex, int subId, boolean msgWaitingInd) { try { @@ -477,6 +494,7 @@ public class TelephonyRegistryManager { * @param subId for which call forwarding status changed. * @param callForwardInd {@code true} indicates there is call forwarding, {@code false} * otherwise. + * @hide */ public void notifyCallForwardingChanged(int subId, boolean callForwardInd) { try { @@ -493,6 +511,7 @@ public class TelephonyRegistryManager { * @param subId for which data activity state changed. * @param dataActivityType indicates the latest data activity type e.g. {@link * TelephonyManager#DATA_ACTIVITY_IN} + * @hide */ public void notifyDataActivityChanged(int subId, @DataActivityType int dataActivityType) { try { @@ -511,6 +530,7 @@ public class TelephonyRegistryManager { * @param subId for which data activity state changed. * @param dataActivityType indicates the latest data activity type e.g. {@link * TelephonyManager#DATA_ACTIVITY_IN} + * @hide */ public void notifyDataActivityChanged(int slotIndex, int subId, @DataActivityType int dataActivityType) { @@ -532,6 +552,7 @@ public class TelephonyRegistryManager { * * @see PreciseDataConnectionState * @see TelephonyManager#DATA_DISCONNECTED + * @hide */ public void notifyDataConnectionForSubscriber(int slotIndex, int subId, @NonNull PreciseDataConnectionState preciseState) { @@ -552,6 +573,7 @@ public class TelephonyRegistryManager { * @param subId for which call quality state changed. * @param callQuality Information about call quality e.g, call quality level * @param networkType associated with this data connection. e.g, LTE + * @hide */ public void notifyCallQualityChanged(int slotIndex, int subId, @NonNull CallQuality callQuality, @NetworkType int networkType) { @@ -573,6 +595,7 @@ public class TelephonyRegistryManager { * {@link CarrierConfigManager#KEY_VOICE_RTP_THRESHOLDS_JITTER_INT} * * @param status media quality status + * @hide */ public void notifyMediaQualityStatusChanged( int slotIndex, int subId, @NonNull MediaQualityStatus status) { @@ -590,6 +613,7 @@ public class TelephonyRegistryManager { * @param slotIndex for which emergency number list changed. Can be derived from subId except * when subId is invalid. * @param subId for which emergency number list changed. + * @hide */ public void notifyEmergencyNumberList( int slotIndex, int subId) { try { @@ -602,14 +626,16 @@ public class TelephonyRegistryManager { /** * Notify outgoing emergency call. - * @param phoneId Sender phone ID. + * @param simSlotIndex Sender phone ID. * @param subId Sender subscription ID. * @param emergencyNumber Emergency number. + * @hide */ - public void notifyOutgoingEmergencyCall(int phoneId, int subId, + @SystemApi + public void notifyOutgoingEmergencyCall(int simSlotIndex, int subId, @NonNull EmergencyNumber emergencyNumber) { try { - sRegistry.notifyOutgoingEmergencyCall(phoneId, subId, emergencyNumber); + sRegistry.notifyOutgoingEmergencyCall(simSlotIndex, subId, emergencyNumber); } catch (RemoteException ex) { // system process is dead throw ex.rethrowFromSystemServer(); @@ -621,6 +647,7 @@ public class TelephonyRegistryManager { * @param phoneId Sender phone ID. * @param subId Sender subscription ID. * @param emergencyNumber Emergency number. + * @hide */ public void notifyOutgoingEmergencySms(int phoneId, int subId, @NonNull EmergencyNumber emergencyNumber) { @@ -639,6 +666,7 @@ public class TelephonyRegistryManager { * subId is invalid. * @param subId for which radio power state changed. * @param radioPowerState the current modem radio state. + * @hide */ public void notifyRadioPowerStateChanged(int slotIndex, int subId, @RadioPowerState int radioPowerState) { @@ -654,6 +682,7 @@ public class TelephonyRegistryManager { * Notify {@link PhoneCapability} changed. * * @param phoneCapability the capability of the modem group. + * @hide */ public void notifyPhoneCapabilityChanged(@NonNull PhoneCapability phoneCapability) { try { @@ -685,6 +714,7 @@ public class TelephonyRegistryManager { * when subId is invalid. * @param subId for which data activation state changed. * @param activationState sim activation state e.g, activated. + * @hide */ public void notifyDataActivationStateChanged(int slotIndex, int subId, @SimActivationState int activationState) { @@ -705,6 +735,7 @@ public class TelephonyRegistryManager { * subId is invalid. * @param subId for which voice activation state changed. * @param activationState sim activation state e.g, activated. + * @hide */ public void notifyVoiceActivationStateChanged(int slotIndex, int subId, @SimActivationState int activationState) { @@ -725,6 +756,7 @@ public class TelephonyRegistryManager { * when subId is invalid. * @param subId for which mobile data state has changed. * @param state {@code true} indicates mobile data is enabled/on. {@code false} otherwise. + * @hide */ public void notifyUserMobileDataStateChanged(int slotIndex, int subId, boolean state) { try { @@ -743,6 +775,7 @@ public class TelephonyRegistryManager { * when the device is in emergency-only mode. * @param subscriptionId Subscription id for which display network info has changed. * @param telephonyDisplayInfo The display info. + * @hide */ public void notifyDisplayInfoChanged(int slotIndex, int subscriptionId, @NonNull TelephonyDisplayInfo telephonyDisplayInfo) { @@ -759,6 +792,7 @@ public class TelephonyRegistryManager { * * @param subId for which ims call disconnect. * @param imsReasonInfo the reason for ims call disconnect. + * @hide */ public void notifyImsDisconnectCause(int subId, @NonNull ImsReasonInfo imsReasonInfo) { try { @@ -775,6 +809,7 @@ public class TelephonyRegistryManager { * * @param subId for which srvcc state changed. * @param state srvcc state + * @hide */ public void notifySrvccStateChanged(int subId, @SrvccState int state) { try { @@ -798,6 +833,7 @@ public class TelephonyRegistryManager { * @param imsServiceTypes Array of IMS call service type for ringing, foreground & * background calls. * @param imsCallTypes Array of IMS call type for ringing, foreground & background calls. + * @hide */ public void notifyPreciseCallState(int slotIndex, int subId, @Annotation.PreciseCallStates int[] callStates, String[] imsCallIds, @@ -822,6 +858,7 @@ public class TelephonyRegistryManager { * @param cause {@link DisconnectCause} for the disconnected call. * @param preciseCause {@link android.telephony.PreciseDisconnectCause} for the disconnected * call. + * @hide */ public void notifyDisconnectCause(int slotIndex, int subId, @DisconnectCauses int cause, @PreciseDisconnectCauses int preciseCause) { @@ -838,6 +875,7 @@ public class TelephonyRegistryManager { * * <p>To be compatible with {@link TelephonyRegistry}, use {@link CellIdentity} which is * parcelable, and convert to CellLocation in client code. + * @hide */ public void notifyCellLocation(int subId, @NonNull CellIdentity cellLocation) { try { @@ -854,6 +892,7 @@ public class TelephonyRegistryManager { * * @param subId for which cellinfo changed. * @param cellInfo A list of cellInfo associated with the given subscription. + * @hide */ public void notifyCellInfoChanged(int subId, @NonNull List<CellInfo> cellInfo) { try { @@ -866,6 +905,7 @@ public class TelephonyRegistryManager { /** * Notify that the active data subscription ID has changed. * @param activeDataSubId The new subscription ID for active data + * @hide */ public void notifyActiveDataSubIdChanged(int activeDataSubId) { try { @@ -896,6 +936,7 @@ public class TelephonyRegistryManager { * For UMTS, if a combined attach succeeds for PS only, then the GMM cause code shall be * included as an additionalCauseCode. For LTE (ESM), cause codes are in * TS 24.301 9.9.4.4. Integer.MAX_VALUE if this value is unused. + * @hide */ public void notifyRegistrationFailed(int slotIndex, int subId, @NonNull CellIdentity cellIdentity, @NonNull String chosenPlmn, @@ -914,6 +955,7 @@ public class TelephonyRegistryManager { * @param slotIndex for the phone object that got updated barring info. * @param subId for which the BarringInfo changed. * @param barringInfo updated BarringInfo. + * @hide */ public void notifyBarringInfoChanged( int slotIndex, int subId, @NonNull BarringInfo barringInfo) { @@ -931,6 +973,7 @@ public class TelephonyRegistryManager { * @param slotIndex for which physical channel configs changed. * @param subId the subId * @param configs a list of {@link PhysicalChannelConfig}, the configs of physical channel. + * @hide */ public void notifyPhysicalChannelConfigForSubscriber(int slotIndex, int subId, List<PhysicalChannelConfig> configs) { @@ -948,6 +991,7 @@ public class TelephonyRegistryManager { * @param enabled True if data is enabled, otherwise disabled. * @param reason Reason for data enabled/disabled. See {@code REASON_*} in * {@link TelephonyManager}. + * @hide */ public void notifyDataEnabled(int slotIndex, int subId, boolean enabled, @TelephonyManager.DataEnabledReason int reason) { @@ -966,6 +1010,7 @@ public class TelephonyRegistryManager { * @param subId for which allowed network types changed. * @param reason an allowed network type reasons. * @param allowedNetworkType an allowed network type bitmask value. + * @hide */ public void notifyAllowedNetworkTypesChanged(int slotIndex, int subId, int reason, long allowedNetworkType) { @@ -983,6 +1028,7 @@ public class TelephonyRegistryManager { * @param slotIndex for the phone object that gets the updated link capacity estimate * @param subId for subscription that gets the updated link capacity estimate * @param linkCapacityEstimateList a list of {@link LinkCapacityEstimate} + * @hide */ public void notifyLinkCapacityEstimateChanged(int slotIndex, int subId, List<LinkCapacityEstimate> linkCapacityEstimateList) { @@ -994,6 +1040,29 @@ public class TelephonyRegistryManager { } } + /** + * Notify external listeners that the subscriptions supporting simultaneous cellular calling + * have changed. + * @param subIds The new set of subIds supporting simultaneous cellular calling. + * @hide + */ + public void notifySimultaneousCellularCallingSubscriptionsChanged( + @NonNull Set<Integer> subIds) { + try { + sRegistry.notifySimultaneousCellularCallingSubscriptionsChanged( + subIds.stream().mapToInt(i -> i).toArray()); + } catch (RemoteException ex) { + // system server crash + throw ex.rethrowFromSystemServer(); + } + } + + /** + * Processes potential event changes from the provided {@link TelephonyCallback}. + * + * @param telephonyCallback callback for monitoring callback changes to the telephony state. + * @hide + */ public @NonNull Set<Integer> getEventsFromCallback( @NonNull TelephonyCallback telephonyCallback) { Set<Integer> eventList = new ArraySet<>(); @@ -1135,7 +1204,11 @@ public class TelephonyRegistryManager { eventList.add(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED); } - + if (telephonyCallback + instanceof TelephonyCallback.SimultaneousCellularCallingSupportListener) { + eventList.add( + TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); + } return eventList; } @@ -1300,6 +1373,7 @@ public class TelephonyRegistryManager { * may encounter an {@link IllegalStateException} when trying to register more callbacks. * * @param callback The {@link TelephonyCallback} object to register. + * @hide */ public void registerTelephonyCallback(boolean renounceFineLocationAccess, boolean renounceCoarseLocationAccess, @@ -1319,6 +1393,7 @@ public class TelephonyRegistryManager { * Unregister an existing {@link TelephonyCallback}. * * @param callback The {@link TelephonyCallback} object to unregister. + * @hide */ public void unregisterTelephonyCallback(int subId, String pkgName, String attributionTag, @NonNull TelephonyCallback callback, boolean notifyNow) { @@ -1379,6 +1454,7 @@ public class TelephonyRegistryManager { * @param logicalSlotIndex The SIM slot to listen on * @param executor The executor where {@code listener} will be invoked * @param callback The callback to register + * @hide */ public void addCarrierPrivilegesCallback( int logicalSlotIndex, @@ -1413,6 +1489,7 @@ public class TelephonyRegistryManager { * Unregisters a {@link CarrierPrivilegesCallback}. * * @param callback The callback to unregister + * @hide */ public void removeCarrierPrivilegesCallback(@NonNull CarrierPrivilegesCallback callback) { if (callback == null) { @@ -1438,6 +1515,7 @@ public class TelephonyRegistryManager { * @param logicalSlotIndex The SIM slot the change occurred on * @param privilegedPackageNames The updated set of packages names with carrier privileges * @param privilegedUids The updated set of UIDs with carrier privileges + * @hide */ public void notifyCarrierPrivilegesChanged( int logicalSlotIndex, @@ -1463,6 +1541,7 @@ public class TelephonyRegistryManager { * @param logicalSlotIndex the SIM slot the change occurred on * @param packageName the package name of the changed {@link CarrierService} * @param uid the UID of the changed {@link CarrierService} + * @hide */ public void notifyCarrierServiceChanged(int logicalSlotIndex, @Nullable String packageName, int uid) { @@ -1479,6 +1558,7 @@ public class TelephonyRegistryManager { * * @param executor The executor on which the callback will be executed. * @param listener The CarrierConfigChangeListener to be registered with. + * @hide */ public void addCarrierConfigChangedListener( @NonNull @CallbackExecutor Executor executor, @@ -1519,6 +1599,7 @@ public class TelephonyRegistryManager { * Unregister to stop the notification when carrier configurations changed. * * @param listener The CarrierConfigChangeListener to be unregistered with. + * @hide */ public void removeCarrierConfigChangedListener( @NonNull CarrierConfigManager.CarrierConfigChangeListener listener) { @@ -1548,6 +1629,7 @@ public class TelephonyRegistryManager { * {@link TelephonyManager#UNKNOWN_CARRIER_ID}. * @param specificCarrierId The optional specific carrier Id, may be {@link * TelephonyManager#UNKNOWN_CARRIER_ID}. + * @hide */ public void notifyCarrierConfigChanged(int slotIndex, int subId, int carrierId, int specificCarrierId) { @@ -1569,6 +1651,7 @@ public class TelephonyRegistryManager { * @param subId Sender subscription ID. * @param type for callback mode entry. * See {@link TelephonyManager.EmergencyCallbackModeType}. + * @hide */ public void notifyCallBackModeStarted(int phoneId, int subId, @TelephonyManager.EmergencyCallbackModeType int type) { @@ -1589,6 +1672,7 @@ public class TelephonyRegistryManager { * See {@link TelephonyManager.EmergencyCallbackModeType}. * @param reason for changing callback mode. * See {@link TelephonyManager.EmergencyCallbackModeStopReason}. + * @hide */ public void notifyCallbackModeStopped(int phoneId, int subId, @TelephonyManager.EmergencyCallbackModeType int type, diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java index ac5eb3cbeeaa..b268c2edd9a7 100644 --- a/core/java/android/text/MeasuredParagraph.java +++ b/core/java/android/text/MeasuredParagraph.java @@ -671,9 +671,18 @@ public class MeasuredParagraph { if (mLtrWithoutBidi) { // If the whole text is LTR direction, just apply whole region. if (builder == null) { - mWholeWidth += paint.getTextRunAdvances( - mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */, - mWidths.getRawArray(), start); + // For the compatibility reasons, the letter spacing should not be dropped at the + // left and right edge. + int oldFlag = paint.getFlags(); + paint.setFlags(paint.getFlags() + | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + mWholeWidth += paint.getTextRunAdvances( + mCopiedBuffer, start, end - start, start, end - start, + false /* isRtl */, mWidths.getRawArray(), start); + } finally { + paint.setFlags(oldFlag); + } } else { builder.appendStyleRun(paint, config, end - start, false /* isRtl */); } @@ -690,9 +699,16 @@ public class MeasuredParagraph { final boolean isRtl = (level & 0x1) != 0; if (builder == null) { final int levelLength = levelEnd - levelStart; - mWholeWidth += paint.getTextRunAdvances( - mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, - isRtl, mWidths.getRawArray(), levelStart); + int oldFlag = paint.getFlags(); + paint.setFlags(paint.getFlags() + | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + mWholeWidth += paint.getTextRunAdvances( + mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, + isRtl, mWidths.getRawArray(), levelStart); + } finally { + paint.setFlags(oldFlag); + } } else { builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl); } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 135935cb0632..2175b47e149e 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -291,6 +291,97 @@ public class TextLine { } /** + * Returns the run flag of at the given BiDi run. + * + * @param bidiRunIndex a BiDi run index. + * @return a run flag of the given BiDi run. + */ + @VisibleForTesting + public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) { + if (bidiRunCount == 1) { + // Easy case. If there is only single run, it is most left and most right run. + return Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + if (bidiRunIndex != 0 && bidiRunIndex != (bidiRunCount - 1)) { + // Easy case. If the given run is the middle of the line, it is not the most left or + // the most right run. + return 0; + } + + int runFlag = 0; + // For the historical reasons, the BiDi implementation of Android works differently + // from the Java BiDi APIs. The mDirections holds the BiDi runs in visual order, but + // it is reversed order if the paragraph direction is RTL. So, the first BiDi run of + // mDirections is located the most left of the line if the paragraph direction is LTR. + // If the paragraph direction is RTL, the first BiDi run is located the most right of + // the line. + if (bidiRunIndex == 0) { + if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) { + runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } else { + runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + } + if (bidiRunIndex == (bidiRunCount - 1)) { + if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) { + runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } else { + runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } + } + return runFlag; + } + + /** + * Resolve the runFlag for the inline span range. + * + * @param runFlag the runFlag of the current BiDi run. + * @param isRtlRun true for RTL run, false for LTR run. + * @param runStart the inclusive BiDi run start offset. + * @param runEnd the exclusive BiDi run end offset. + * @param spanStart the inclusive span start offset. + * @param spanEnd the exclusive span end offset. + * @return the resolved runFlag. + */ + @VisibleForTesting + public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart, + int runEnd, int spanStart, int spanEnd) { + if (runFlag == 0) { + // Easy case. If the run is in the middle of the line, any inline span is also in the + // middle of the line. + return 0; + } + int localRunFlag = runFlag; + if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) != 0) { + if (isRtlRun) { + if (spanEnd != runEnd) { + // In the RTL context, the last run is the most left run. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } + } else { // LTR + if (spanStart != runStart) { + // In the LTR context, the first run is the most left run. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } + } + } + if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) != 0) { + if (isRtlRun) { + if (spanStart != runStart) { + // In the RTL context, the start of the run is the most right run. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + } else { // LTR + if (spanEnd != runEnd) { + // In the LTR context, the last run is the most right position. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + } + } + return localRunFlag; + } + + /** * Renders the TextLine. * * @param c the canvas to render on @@ -308,11 +399,13 @@ public class TextLine { final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); + int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom, - runIndex != (runCount - 1) || j != mLen); + runIndex != (runCount - 1) || j != mLen, runFlag); if (j != runLimit) { // charAt(j) == TAB_CHAR h = mDir * nextTab(h * mDir); @@ -371,11 +464,12 @@ public class TextLine { final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { horizontal += shapeRun(consumer, segStart, j, runIsRtl, x + horizontal, - runIndex != (runCount - 1) || j != mLen); + runIndex != (runCount - 1) || j != mLen, runFlag); if (j != runLimit) { // charAt(j) == TAB_CHAR horizontal = mDir * nextTab(horizontal * mDir); @@ -441,11 +535,13 @@ public class TextLine { } float h = 0; - for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { + final int runCount = mDirections.getRunCount(); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final int runStart = mDirections.getRunStart(runIndex); if (runStart > mLen) break; final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { @@ -455,16 +551,16 @@ public class TextLine { if (targetIsInThisSegment && sameDirection) { return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null, - 0, h, lineInfo); + 0, h, lineInfo, runFlag); } final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds, - null, 0, h, lineInfo); + null, 0, h, lineInfo, runFlag); h += sameDirection ? segmentWidth : -segmentWidth; if (targetIsInThisSegment) { return h + measureRun(segStart, offset, j, runIsRtl, null, null, null, 0, - h, lineInfo); + h, lineInfo, runFlag); } if (j != runLimit) { // charAt(j) == TAB_CHAR @@ -543,20 +639,21 @@ public class TextLine { + "result, needed: " + mLen + " had: " + advances.length); } float h = 0; - for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { + final int runCount = mDirections.getRunCount(); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final int runStart = mDirections.getRunStart(runIndex); if (runStart > mLen) break; final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; - final float segmentWidth = measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0, - null); + null, runFlag); final float oldh = h; h += sameDirection ? segmentWidth : -segmentWidth; @@ -608,11 +705,13 @@ public class TextLine { } float horizontal = 0; - for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { + final int runCount = mDirections.getRunCount(); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final int runStart = mDirections.getRunStart(runIndex); if (runStart > mLen) break; final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) { @@ -629,7 +728,7 @@ public class TextLine { final float previousSegEndHorizontal = measurement[segStart]; final float width = measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart, - 0, null); + 0, null, runFlag); horizontal += sameDirection ? width : -width; float currHorizontal = sameDirection ? oldHorizontal : horizontal; @@ -686,22 +785,24 @@ public class TextLine { * @param y the baseline * @param bottom the bottom of the line * @param needWidth true if the width value is required. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run, based on the paragraph direction. * Only valid if needWidth is true. */ private float drawRun(Canvas c, int start, int limit, boolean runIsRtl, float x, int top, int y, int bottom, - boolean needWidth) { + boolean needWidth, int runFlag) { if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null); + float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null, + runFlag); handleRun(start, limit, limit, runIsRtl, c, null, x + w, top, - y, bottom, null, null, false, null, 0, null); + y, bottom, null, null, false, null, 0, null, runFlag); return w; } return handleRun(start, limit, limit, runIsRtl, c, null, x, top, - y, bottom, null, null, needWidth, null, 0, null); + y, bottom, null, null, needWidth, null, 0, null, runFlag); } /** @@ -718,19 +819,21 @@ public class TextLine { * @param advancesIndex the start index to fill in the advance information. * @param x horizontal offset of the run. * @param lineInfo an optional output parameter for filling line information. + * @param runFlag the run flag to be applied for this run. * @return the signed width from the start of the run to the leading edge * of the character at offset, based on the run (not paragraph) direction */ private float measureRun(int start, int offset, int limit, boolean runIsRtl, @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances, - int advancesIndex, float x, @Nullable LineInfo lineInfo) { + int advancesIndex, float x, @Nullable LineInfo lineInfo, int runFlag) { if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null); + float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null, + runFlag); return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi, - drawBounds, true, advances, advancesIndex, lineInfo); + drawBounds, true, advances, advancesIndex, lineInfo, runFlag); } return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds, - true, advances, advancesIndex, lineInfo); + true, advances, advancesIndex, lineInfo, runFlag); } /** @@ -742,21 +845,23 @@ public class TextLine { * @param runIsRtl true if the run is right-to-left * @param x the position of the run that is closest to the leading margin * @param needWidth true if the width value is required. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run, based on the paragraph direction. * Only valid if needWidth is true. */ private float shapeRun(TextShaper.GlyphsConsumer consumer, int start, - int limit, boolean runIsRtl, float x, boolean needWidth) { + int limit, boolean runIsRtl, float x, boolean needWidth, int runFlag) { if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null); + float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null, + runFlag); handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null, - false, null, 0, null); + false, null, 0, null, runFlag); return w; } return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null, - needWidth, null, 0, null); + needWidth, null, 0, null, runFlag); } @@ -1160,6 +1265,7 @@ public class TextLine { * @param advances receives the advance information about the requested run, can be null. * @param advancesIndex the start index to fill in the advance information. * @param lineInfo an optional output parameter for filling line information. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run based on the run direction; only * valid if needWidth is true */ @@ -1168,8 +1274,8 @@ public class TextLine { Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations, - @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) { - + @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, + int runFlag) { if (mIsJustifying) { wp.setWordSpacing(mAddedWidthForJustify); } @@ -1187,7 +1293,16 @@ public class TextLine { } float totalWidth = 0; - + if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) == Paint.TEXT_RUN_FLAG_LEFT_EDGE) { + wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_LEFT_EDGE); + } else { + wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_LEFT_EDGE); + } + if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) == Paint.TEXT_RUN_FLAG_RIGHT_EDGE) { + wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_RIGHT_EDGE); + } else { + wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE); + } final int numDecorations = decorations == null ? 0 : decorations.size(); if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) { @@ -1419,6 +1534,7 @@ public class TextLine { * @param advances receives the advance information about the requested run, can be null. * @param advancesIndex the start index to fill in the advance information. * @param lineInfo an optional output parameter for filling line information. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run based on the run direction; only * valid if needWidth is true */ @@ -1426,7 +1542,8 @@ public class TextLine { int limit, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, - @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) { + @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, + int runFlag) { if (measureLimit < start || measureLimit > limit) { throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " @@ -1473,7 +1590,7 @@ public class TextLine { wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit())); return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top, y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances, - advancesIndex, lineInfo); + advancesIndex, lineInfo, runFlag); } // Shaping needs to take into account context up to metric boundaries, @@ -1554,6 +1671,9 @@ public class TextLine { // and use. activePaint.set(wp); } else if (!equalAttributes(wp, activePaint)) { + final int spanRunFlag = resolveRunFlagForSubSequence( + runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd); + // The style of the present chunk of text is substantially different from the // style of the previous chunk. We need to handle the active piece of text // and restart with the present chunk. @@ -1565,7 +1685,7 @@ public class TextLine { consumer, x, top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit, Math.min(activeEnd, mlimit), mDecorations, - advances, advancesIndex + activeStart - start, lineInfo); + advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag); activeStart = j; activePaint.set(wp); @@ -1585,6 +1705,9 @@ public class TextLine { mDecorations.add(copy); } } + + final int spanRunFlag = resolveRunFlagForSubSequence( + runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd); // Handle the final piece of text. activePaint.setStartHyphenEdit( adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); @@ -1593,7 +1716,7 @@ public class TextLine { x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x, top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit, Math.min(activeEnd, mlimit), mDecorations, - advances, advancesIndex + activeStart - start, lineInfo); + advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag); } return x - originalX; @@ -1614,7 +1737,6 @@ public class TextLine { */ private void drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { - if (mCharsValid) { int count = end - start; int contextCount = contextEnd - contextStart; diff --git a/core/java/android/tracing/transition/TransitionDataSource.java b/core/java/android/tracing/transition/TransitionDataSource.java index b8daace30d0a..baece75cbf09 100644 --- a/core/java/android/tracing/transition/TransitionDataSource.java +++ b/core/java/android/tracing/transition/TransitionDataSource.java @@ -16,6 +16,7 @@ package android.tracing.transition; +import android.tracing.perfetto.CreateTlsStateArgs; import android.tracing.perfetto.DataSource; import android.tracing.perfetto.DataSourceInstance; import android.tracing.perfetto.FlushCallbackArguments; @@ -23,10 +24,14 @@ import android.tracing.perfetto.StartCallbackArguments; import android.tracing.perfetto.StopCallbackArguments; import android.util.proto.ProtoInputStream; +import java.util.HashMap; +import java.util.Map; + /** * @hide */ -public class TransitionDataSource extends DataSource { +public class TransitionDataSource + extends DataSource<DataSourceInstance, TransitionDataSource.TlsState, Void> { public static String DATA_SOURCE_NAME = "com.android.wm.shell.transition"; private final Runnable mOnStartStaticCallback; @@ -41,6 +46,15 @@ public class TransitionDataSource extends DataSource { } @Override + protected TlsState createTlsState(CreateTlsStateArgs<DataSourceInstance> args) { + return new TlsState(); + } + + public class TlsState { + public final Map<String, Integer> handlerMapping = new HashMap<>(); + } + + @Override public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) { return new DataSourceInstance(this, instanceIndex) { @Override diff --git a/core/java/android/view/EventLogTags.logtags b/core/java/android/view/EventLogTags.logtags index cc0b18a652d5..f1cd671ef176 100644 --- a/core/java/android/view/EventLogTags.logtags +++ b/core/java/android/view/EventLogTags.logtags @@ -59,5 +59,10 @@ option java_package android.view # Enqueue Input Event 62002 view_enqueue_input_event (eventType|3),(action|3) +# following other view events defined in system/logging/logcat/event.logtags +# ViewRoot Draw Events +60004 viewroot_draw_event (window|3),(event|3) + + # NOTE - the range 1000000-2000000 is reserved for partners and others who # want to define their own log tags without conflicting with the core platform.
\ No newline at end of file diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 36b74e39072a..7903050e41d4 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -69,12 +69,13 @@ import android.view.SurfaceControl; import android.view.displayhash.DisplayHash; import android.view.displayhash.VerifiedDisplayHash; import android.window.AddToSurfaceSyncGroupResult; +import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; -import android.window.ScreenCapture; -import android.window.WindowContextInfo; import android.window.ITrustedPresentationListener; +import android.window.ScreenCapture; import android.window.TrustedPresentationThresholds; +import android.window.WindowContextInfo; /** * System private interface to the window manager. @@ -1083,4 +1084,8 @@ interface IWindowManager void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); + + boolean registerScreenRecordingCallback(IScreenRecordingCallback callback); + + void unregisterScreenRecordingCallback(IScreenRecordingCallback callback); } diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 785055441d59..ba7874eb2d21 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -39,8 +39,10 @@ import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import android.view.flags.Flags; import dalvik.system.CloseGuard; +import dalvik.system.VMRuntime; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -101,6 +103,10 @@ public class Surface implements Parcelable { long nativeObject, float frameRate, int compatibility, int changeFrameRateStrategy); private static native void nativeDestroy(long nativeObject); + // 5MB is a wild guess for what the average surface should be. On most new phones, a full-screen + // surface is about 9MB... but not all surfaces are screen size. This should be a nice balance. + private static final long SURFACE_NATIVE_ALLOCATION_SIZE_BYTES = 5_000_000; + public static final @android.annotation.NonNull Parcelable.Creator<Surface> CREATOR = new Parcelable.Creator<Surface>() { @Override @@ -331,6 +337,7 @@ public class Surface implements Parcelable { */ @UnsupportedAppUsage public Surface() { + registerNativeMemoryUsage(); } /** @@ -343,6 +350,7 @@ public class Surface implements Parcelable { */ public Surface(@NonNull SurfaceControl from) { copyFrom(from); + registerNativeMemoryUsage(); } /** @@ -370,6 +378,7 @@ public class Surface implements Parcelable { mName = surfaceTexture.toString(); setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture)); } + registerNativeMemoryUsage(); } /* called from android_view_Surface_createFromIGraphicBufferProducer() */ @@ -378,6 +387,7 @@ public class Surface implements Parcelable { synchronized (mLock) { setNativeObjectLocked(nativeObject); } + registerNativeMemoryUsage(); } @Override @@ -389,6 +399,7 @@ public class Surface implements Parcelable { release(); } finally { super.finalize(); + freeNativeMemoryUsage(); } } @@ -1243,4 +1254,16 @@ public class Surface implements Parcelable { return mIsWideColorGamut; } } + + private static void registerNativeMemoryUsage() { + if (Flags.enableSurfaceNativeAllocRegistration()) { + VMRuntime.getRuntime().registerNativeAllocation(SURFACE_NATIVE_ALLOCATION_SIZE_BYTES); + } + } + + private static void freeNativeMemoryUsage() { + if (Flags.enableSurfaceNativeAllocRegistration()) { + VMRuntime.getRuntime().registerNativeFree(SURFACE_NATIVE_ALLOCATION_SIZE_BYTES); + } + } } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 674f22c4166e..3ed03859ffa6 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -294,8 +294,8 @@ public final class SurfaceControl implements Parcelable { private static native void nativeClearTrustedPresentationCallback(long transactionObj, long nativeObject); private static native StalledTransactionInfo nativeGetStalledTransactionInfo(int pid); - private static native void nativeSetDesiredPresentTime(long transactionObj, - long desiredPresentTime); + private static native void nativeSetDesiredPresentTimeNanos(long transactionObj, + long desiredPresentTimeNanos); private static native void nativeSetFrameTimeline(long transactionObj, long vsyncId); @@ -2563,12 +2563,12 @@ public final class SurfaceControl implements Parcelable { */ @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) public static final class TransactionStats { - private long mLatchTime; + private long mLatchTimeNanos; private SyncFence mSyncFence; // called from native - private TransactionStats(long latchTime, long presentFencePtr) { - mLatchTime = latchTime; + private TransactionStats(long latchTimeNanos, long presentFencePtr) { + mLatchTimeNanos = latchTimeNanos; mSyncFence = new SyncFence(presentFencePtr); } @@ -2581,12 +2581,12 @@ public final class SurfaceControl implements Parcelable { } /** - * Returns the timestamp of when the frame was latched by the framework and queued for - * presentation. + * Returns the timestamp (in CLOCK_MONOTONIC) of when the frame was latched by the + * framework and queued for presentation. */ @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) - public long getLatchTime() { - return mLatchTime; + public long getLatchTimeNanos() { + return mLatchTimeNanos; } /** @@ -4426,8 +4426,8 @@ public final class SurfaceControl implements Parcelable { } /** - * Specifies a desiredPresentTime for the transaction. The framework will try to present - * the transaction at or after the time specified. + * Specifies a desiredPresentTimeNanos for the transaction. The framework will try to + * present the transaction at or after the time specified. * * Transactions will not be presented until all of their acquire fences have signaled even * if the app requests an earlier present time. @@ -4436,17 +4436,17 @@ public final class SurfaceControl implements Parcelable { * a desired present time that is before x, the later transaction will not preempt the * earlier transaction. * - * @param desiredPresentTime The desired time (in CLOCK_MONOTONIC) for the transaction. + * @param desiredPresentTimeNanos The desired time (in CLOCK_MONOTONIC) for the transaction. * @return This transaction */ @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) @NonNull - public Transaction setDesiredPresentTime(long desiredPresentTime) { + public Transaction setDesiredPresentTimeNanos(long desiredPresentTimeNanos) { if (!Flags.sdkDesiredPresentTime()) { Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled"); return this; } - nativeSetDesiredPresentTime(mNativeObject, desiredPresentTime); + nativeSetDesiredPresentTimeNanos(mNativeObject, desiredPresentTimeNanos); return this; } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 257ecc565c87..c98d1d7ecaea 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -15015,6 +15015,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** @hide */ + @Nullable View getSelfOrParentImportantForA11y() { if (isImportantForAccessibility()) return this; ViewParent parent = getParentForAccessibility(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c66f3c8032fd..e0bda9181380 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -26,6 +26,7 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_NONE; import static android.view.InsetsSource.ID_IME; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; @@ -87,6 +88,7 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_B import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; +import static android.view.accessibility.Flags.fixMergedContentChangeEvent; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; @@ -285,6 +287,7 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_TOUCH_NAVIGATION = false || LOCAL_LOGV; private static final boolean DEBUG_BLAST = false || LOCAL_LOGV; private static final int LOGTAG_INPUT_FOCUS = 62001; + private static final int LOGTAG_VIEWROOT_DRAW_EVENT = 60004; /** * Set to false if we do not want to use the multi threaded renderer even though @@ -1009,8 +1012,10 @@ public final class ViewRootImpl implements ViewParent, // Used to check if there were any view invalidations in // the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME). private boolean mHasInvalidation = false; - // Used to check if it is in the touch boosting period. + // Used to check if it is in the frame rate boosting period. private boolean mIsFrameRateBoosting = false; + // Used to check if it is in touch boosting period. + private boolean mIsTouchBoosting = false; // Used to check if there is a message in the message queue // for idleness handling. private boolean mHasIdledMessage = false; @@ -4749,13 +4754,7 @@ public final class ViewRootImpl implements ViewParent, } private void reportDrawFinished(@Nullable Transaction t, int seqId) { - if (DEBUG_BLAST) { - Log.d(mTag, "reportDrawFinished"); - } - - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { - Trace.instant(Trace.TRACE_TAG_VIEW, "reportDrawFinished " + mTag + " seqId=" + seqId); - } + logAndTrace("reportDrawFinished seqId=" + seqId); try { mWindowSession.finishDrawing(mWindow, t, seqId); } catch (RemoteException e) { @@ -6425,11 +6424,12 @@ public final class ViewRootImpl implements ViewParent, * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME). */ mIsFrameRateBoosting = false; + mIsTouchBoosting = false; setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory, mLastPreferredFrameRateCategory)); break; case MSG_CHECK_INVALIDATION_IDLE: - if (!mHasInvalidation && !mIsFrameRateBoosting) { + if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) { mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; setPreferredFrameRateCategory(mPreferredFrameRateCategory); mHasIdledMessage = false; @@ -7452,7 +7452,7 @@ public final class ViewRootImpl implements ViewParent, // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { // set the frame rate to the maximum value. - mIsFrameRateBoosting = true; + mIsTouchBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); } /** @@ -11509,6 +11509,15 @@ public final class ViewRootImpl implements ViewParent, event.setContentChangeTypes(mChangeTypes); if (mAction.isPresent()) event.setAction(mAction.getAsInt()); if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin; + + if (fixMergedContentChangeEvent()) { + if ((mChangeTypes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { + final View importantParent = source.getSelfOrParentImportantForA11y(); + if (importantParent != null) { + source = importantParent; + } + } + } source.sendAccessibilityEventUnchecked(event); } else { mLastEventTimeMillis = 0; @@ -11543,14 +11552,29 @@ public final class ViewRootImpl implements ViewParent, } if (mSource != null) { - // If there is no common predecessor, then mSource points to - // a removed view, hence in this case always prefer the source. - View predecessor = getCommonPredecessor(mSource, source); - if (predecessor != null) { - predecessor = predecessor.getSelfOrParentImportantForA11y(); + if (fixMergedContentChangeEvent()) { + View newSource = getCommonPredecessor(mSource, source); + if (newSource == null) { + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + newSource = source; + } + + mChangeTypes |= changeType; + if (mSource != newSource) { + mChangeTypes |= AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; + mSource = newSource; + } + } else { + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + View predecessor = getCommonPredecessor(mSource, source); + if (predecessor != null) { + predecessor = predecessor.getSelfOrParentImportantForA11y(); + } + mSource = (predecessor != null) ? predecessor : source; + mChangeTypes |= changeType; } - mSource = (predecessor != null) ? predecessor : source; - mChangeTypes |= changeType; final int performingAction = mAccessibilityManager.getPerformingAction(); if (performingAction != 0) { @@ -12169,7 +12193,10 @@ public final class ViewRootImpl implements ViewParent, if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.instant(Trace.TRACE_TAG_VIEW, mTag + "-" + msg); } - Log.d(mTag, msg); + if (DEBUG_BLAST) { + Log.d(mTag, msg); + } + EventLog.writeEvent(LOGTAG_VIEWROOT_DRAW_EVENT, mTag, msg); } private void setPreferredFrameRateCategory(int preferredFrameRateCategory) { @@ -12177,8 +12204,16 @@ public final class ViewRootImpl implements ViewParent, return; } - int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning - ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory; + int frameRateCategory = mIsTouchBoosting + ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory; + + // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT + // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction. + // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction + // (e.g., Window Initialization). + if (mIsFrameRateBoosting || mInsetsAnimationRunning) { + frameRateCategory = FRAME_RATE_CATEGORY_HIGH; + } try { if (mLastPreferredFrameRateCategory != frameRateCategory) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index c7355c144c5f..efae57c9946c 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -53,6 +53,13 @@ flag { flag { namespace: "accessibility" + name: "fix_merged_content_change_event" + description: "Fixes event type and source of content change event merged in ViewRootImpl" + bug: "277305460" +} + +flag { + namespace: "accessibility" name: "flash_notification_system_api" description: "Makes flash notification APIs as system APIs for calling from mainline module" bug: "303131332" diff --git a/core/java/android/window/IScreenRecordingCallback.aidl b/core/java/android/window/IScreenRecordingCallback.aidl new file mode 100644 index 000000000000..560ee75aa365 --- /dev/null +++ b/core/java/android/window/IScreenRecordingCallback.aidl @@ -0,0 +1,24 @@ +/* + * 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 android.window; + +/** + * @hide + */ +oneway interface IScreenRecordingCallback { + void onScreenRecordingStateChanged(boolean visibleInScreenRecording); +} diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index bceb8726b1cb..feae173f3e61 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -421,8 +421,11 @@ public final class TransitionInfo implements Parcelable { final String perChangeLineStart = shouldPrettyPrint ? "\n" + innerPrefix : ""; StringBuilder sb = new StringBuilder(); sb.append("{id=").append(mDebugId).append(" t=").append(transitTypeToString(mType)) - .append(" f=0x").append(Integer.toHexString(mFlags)).append(" trk=").append(mTrack) - .append(" r=["); + .append(" f=0x").append(Integer.toHexString(mFlags)).append(" trk=").append(mTrack); + if (mOptions != null) { + sb.append(" opt=").append(mOptions); + } + sb.append(" r=["); for (int i = 0; i < mRoots.size(); ++i) { if (i > 0) { sb.append(','); @@ -1211,21 +1214,31 @@ public final class TransitionInfo implements Parcelable { @NonNull private static String typeToString(int mode) { - switch(mode) { - case ANIM_CUSTOM: return "ANIM_CUSTOM"; - case ANIM_CLIP_REVEAL: return "ANIM_CLIP_REVEAL"; - case ANIM_SCALE_UP: return "ANIM_SCALE_UP"; - case ANIM_THUMBNAIL_SCALE_UP: return "ANIM_THUMBNAIL_SCALE_UP"; - case ANIM_THUMBNAIL_SCALE_DOWN: return "ANIM_THUMBNAIL_SCALE_DOWN"; - case ANIM_OPEN_CROSS_PROFILE_APPS: return "ANIM_OPEN_CROSS_PROFILE_APPS"; - default: return "<unknown:" + mode + ">"; - } + return switch (mode) { + case ANIM_CUSTOM -> "CUSTOM"; + case ANIM_SCALE_UP -> "SCALE_UP"; + case ANIM_THUMBNAIL_SCALE_UP -> "THUMBNAIL_SCALE_UP"; + case ANIM_THUMBNAIL_SCALE_DOWN -> "THUMBNAIL_SCALE_DOWN"; + case ANIM_SCENE_TRANSITION -> "SCENE_TRANSITION"; + case ANIM_CLIP_REVEAL -> "CLIP_REVEAL"; + case ANIM_OPEN_CROSS_PROFILE_APPS -> "OPEN_CROSS_PROFILE_APPS"; + case ANIM_FROM_STYLE -> "FROM_STYLE"; + default -> "<" + mode + ">"; + }; } @Override public String toString() { - return "{ AnimationOptions type= " + typeToString(mType) + " package=" + mPackageName - + " override=" + mOverrideTaskTransition + " b=" + mTransitionBounds + "}"; + final StringBuilder sb = new StringBuilder(32); + sb.append("{t=").append(typeToString(mType)); + if (mOverrideTaskTransition) { + sb.append(" overrideTask=true"); + } + if (!mTransitionBounds.isEmpty()) { + sb.append(" bounds=").append(mTransitionBounds); + } + sb.append('}'); + return sb.toString(); } /** Customized activity transition. */ diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig index ad0e9a487c53..f67eefa2281f 100644 --- a/core/java/android/window/flags/responsible_apis.aconfig +++ b/core/java/android/window/flags/responsible_apis.aconfig @@ -8,6 +8,13 @@ flag { } flag { + name: "bal_require_opt_in_same_uid" + namespace: "responsible_apis" + description: "Require the PendingIntent creator/sender to opt in if it is the same UID" + bug: "296478951" +} + +flag { name: "bal_dont_bring_existing_background_task_stack_to_fg" namespace: "responsible_apis" description: "When starting a PendingIntent with ONLY creator privileges, don't bring the existing task stack to foreground" diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 3c3c8469b3a4..751c1a8dd0ff 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -80,3 +80,11 @@ flag { is_fixed_read_only: true bug: "278757236" } + +flag { + namespace: "window_surfaces" + name: "screen_recording_callbacks" + description: "Enable screen recording callbacks public API" + is_fixed_read_only: true + bug: "304574518" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index bb16ad2cb8de..2c5fbd7f3725 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -69,11 +69,10 @@ flag { } flag { - name: "predictive_back_system_animations" + name: "predictive_back_system_anims" namespace: "systemui" description: "Predictive back for system animations" - bug: "319421778" - is_fixed_read_only: true + bug: "320510464" } flag { @@ -90,4 +89,12 @@ flag { description: "Feature flag to enable a multi-instance system ui component property." bug: "262864589" is_fixed_read_only: true +} + +flag { + name: "insets_decoupled_configuration" + namespace: "windowing_frontend" + description: "Configuration decoupled from insets" + bug: "151861875" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index f743ab74d1f5..6e5807b21d3d 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -46,4 +46,11 @@ flag { description: "To dispatch ActivityWindowInfo through ClientTransaction" bug: "287582673" is_fixed_read_only: true +} + +flag { + namespace: "windowing_sdk" + name: "embedded_activity_back_nav_flag" + description: "Refines embedded activity back navigation behavior" + bug: "240575809" }
\ No newline at end of file diff --git a/core/java/com/android/internal/os/MonotonicClock.java b/core/java/com/android/internal/os/MonotonicClock.java index c3bcfa6b2cc2..eca6f58dd50f 100644 --- a/core/java/com/android/internal/os/MonotonicClock.java +++ b/core/java/com/android/internal/os/MonotonicClock.java @@ -17,11 +17,11 @@ package com.android.internal.os; import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.AtomicFile; import android.util.Log; import android.util.Xml; -import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -49,20 +49,27 @@ public class MonotonicClock { private final AtomicFile mFile; private final Clock mClock; - private long mTimeshift; + private final long mTimeshift; public static final long UNDEFINED = -1; public MonotonicClock(File file) { - mFile = new AtomicFile(file); - mClock = Clock.SYSTEM_CLOCK; - read(); + this (file, Clock.SYSTEM_CLOCK.elapsedRealtime(), Clock.SYSTEM_CLOCK); } public MonotonicClock(long monotonicTime, @NonNull Clock clock) { + this(null, monotonicTime, clock); + } + + public MonotonicClock(@Nullable File file, long monotonicTime, @NonNull Clock clock) { mClock = clock; - mFile = null; - mTimeshift = monotonicTime - mClock.elapsedRealtime(); + if (file != null) { + mFile = new AtomicFile(file); + mTimeshift = read(monotonicTime - mClock.elapsedRealtime()); + } else { + mFile = null; + mTimeshift = monotonicTime - mClock.elapsedRealtime(); + } } /** @@ -81,15 +88,16 @@ public class MonotonicClock { return mTimeshift + elapsedRealtimeMs; } - private void read() { + private long read(long defaultTimeshift) { if (!mFile.exists()) { - return; + return defaultTimeshift; } try { - readXml(new ByteArrayInputStream(mFile.readFully()), Xml.newBinaryPullParser()); + return readXml(new ByteArrayInputStream(mFile.readFully()), Xml.newBinaryPullParser()); } catch (IOException e) { Log.e(TAG, "Cannot load monotonic clock from " + mFile.getBaseFile(), e); + return defaultTimeshift; } } @@ -102,18 +110,21 @@ public class MonotonicClock { return; } - try (FileOutputStream out = mFile.startWrite()) { + FileOutputStream out = null; + try { + out = mFile.startWrite(); writeXml(out, Xml.newBinarySerializer()); + mFile.finishWrite(out); } catch (IOException e) { Log.e(TAG, "Cannot write monotonic clock to " + mFile.getBaseFile(), e); + mFile.failWrite(out); } } /** * Parses an XML file containing the persistent state of the monotonic clock. */ - @VisibleForTesting - public void readXml(InputStream inputStream, TypedXmlPullParser parser) throws IOException { + private long readXml(InputStream inputStream, TypedXmlPullParser parser) throws IOException { long savedTimeshift = 0; try { parser.setInput(inputStream, StandardCharsets.UTF_8.name()); @@ -128,14 +139,13 @@ public class MonotonicClock { } catch (XmlPullParserException e) { throw new IOException(e); } - mTimeshift = savedTimeshift - mClock.elapsedRealtime(); + return savedTimeshift - mClock.elapsedRealtime(); } /** * Creates an XML file containing the persistent state of the monotonic clock. */ - @VisibleForTesting - public void writeXml(OutputStream out, TypedXmlSerializer serializer) throws IOException { + private void writeXml(OutputStream out, TypedXmlSerializer serializer) throws IOException { serializer.setOutput(out, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); serializer.startTag(null, XML_TAG_MONOTONIC_TIME); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java index e3bfb38802db..e419d13730c2 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java @@ -38,7 +38,7 @@ import java.util.List; public class ParsedAttributionImpl implements ParsedAttribution, Parcelable { /** Maximum amount of attributions per package */ - static final int MAX_NUM_ATTRIBUTIONS = 10000; + static final int MAX_NUM_ATTRIBUTIONS = 400; /** Tag of the attribution */ private @NonNull String tag; diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index 03cfd4f2ab91..969f95db002d 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -80,4 +80,5 @@ oneway interface IPhoneStateListener { void onMediaQualityStatusChanged(in MediaQualityStatus mediaQualityStatus); void onCallBackModeStarted(int type); void onCallBackModeStopped(int type, int reason); + void onSimultaneousCallingStateChanged(in int[] subIds); } diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index aab22421b334..0203ea49f252 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -104,6 +104,7 @@ interface ITelephonyRegistry { void notifyAllowedNetworkTypesChanged(in int phoneId, in int subId, in int reason, in long allowedNetworkType); void notifyLinkCapacityEstimateChanged(in int phoneId, in int subId, in List<LinkCapacityEstimate> linkCapacityEstimateList); + void notifySimultaneousCellularCallingSubscriptionsChanged(in int[] subIds); void addCarrierPrivilegesCallback( int phoneId, ICarrierPrivilegesCallback callback, String pkg, String featureId); diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java index b87027cbb6e7..419193688268 100644 --- a/core/java/com/android/internal/util/CollectionUtils.java +++ b/core/java/com/android/internal/util/CollectionUtils.java @@ -120,8 +120,9 @@ public class CollectionUtils { public static @NonNull <I, O> List<O> map(@Nullable List<I> cur, Function<? super I, ? extends O> f) { if (isEmpty(cur)) return Collections.emptyList(); - final ArrayList<O> result = new ArrayList<>(); - for (int i = 0; i < cur.size(); i++) { + final int size = cur.size(); + final ArrayList<O> result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { result.add(f.apply(cur.get(i))); } return result; @@ -133,7 +134,7 @@ public class CollectionUtils { public static @NonNull <I, O> Set<O> map(@Nullable Set<I> cur, Function<? super I, ? extends O> f) { if (isEmpty(cur)) return emptySet(); - ArraySet<O> result = new ArraySet<>(); + ArraySet<O> result = new ArraySet<>(cur.size()); if (cur instanceof ArraySet) { ArraySet<I> arraySet = (ArraySet<I>) cur; int size = arraySet.size(); @@ -163,7 +164,7 @@ public class CollectionUtils { Function<? super I, ? extends O> f) { if (isEmpty(cur)) return Collections.emptyList(); List<O> result = null; - for (int i = 0; i < cur.size(); i++) { + for (int i = 0, size = cur.size(); i < size; i++) { O transformed = f.apply(cur.get(i)); if (transformed != null) { result = add(result, transformed); @@ -239,7 +240,7 @@ public class CollectionUtils { public static @NonNull <T> List<T> filter(@Nullable List<?> list, Class<T> c) { if (isEmpty(list)) return Collections.emptyList(); ArrayList<T> result = null; - for (int i = 0; i < list.size(); i++) { + for (int i = 0, size = list.size(); i < size; i++) { final Object item = list.get(i); if (c.isInstance(item)) { result = ArrayUtils.add(result, (T) item); @@ -273,7 +274,7 @@ public class CollectionUtils { public static @Nullable <T> T find(@Nullable List<T> items, java.util.function.Predicate<T> predicate) { if (isEmpty(items)) return null; - for (int i = 0; i < items.size(); i++) { + for (int i = 0, size = items.size(); i < size; i++) { final T item = items.get(i); if (predicate.test(item)) return item; } diff --git a/core/java/com/android/internal/util/RingBuffer.java b/core/java/com/android/internal/util/RingBuffer.java index 04886598de35..342ba1b67e4d 100644 --- a/core/java/com/android/internal/util/RingBuffer.java +++ b/core/java/com/android/internal/util/RingBuffer.java @@ -19,7 +19,10 @@ package com.android.internal.util; import static com.android.internal.util.Preconditions.checkArgumentPositive; import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; +import java.util.function.IntFunction; +import java.util.function.Supplier; /** * A simple ring buffer structure with bounded capacity backed by an array. @@ -30,16 +33,35 @@ import java.util.Arrays; @android.ravenwood.annotation.RavenwoodKeepWholeClass public class RingBuffer<T> { + private final Supplier<T> mNewItem; // Array for storing events. private final T[] mBuffer; // Cursor keeping track of the logical end of the array. This cursor never // wraps and instead keeps track of the total number of append() operations. private long mCursor = 0; + /** + * @deprecated This uses reflection to create new instances. + * Use {@link #RingBuffer(Supplier, IntFunction, int)}} instead. + */ + @Deprecated public RingBuffer(Class<T> c, int capacity) { + this(() -> (T) createNewItem(c), cap -> (T[]) Array.newInstance(c, cap), capacity); + } + + private static Object createNewItem(Class c) { + try { + return c.getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | NoSuchMethodException + | InvocationTargetException e) { + return null; + } + } + + public RingBuffer(Supplier<T> newItem, IntFunction<T[]> newBacking, int capacity) { checkArgumentPositive(capacity, "A RingBuffer cannot have 0 capacity"); - // Java cannot create generic arrays without a runtime hint. - mBuffer = (T[]) Array.newInstance(c, capacity); + mBuffer = newBacking.apply(capacity); + mNewItem = newItem; } public int size() { @@ -69,22 +91,11 @@ public class RingBuffer<T> { public T getNextSlot() { final int nextSlotIdx = indexOf(mCursor++); if (mBuffer[nextSlotIdx] == null) { - mBuffer[nextSlotIdx] = createNewItem(); + mBuffer[nextSlotIdx] = mNewItem.get(); } return mBuffer[nextSlotIdx]; } - /** - * @return a new object of type <T> or null if a new object could not be created. - */ - protected T createNewItem() { - try { - return (T) mBuffer.getClass().getComponentType().newInstance(); - } catch (IllegalAccessException | InstantiationException e) { - return null; - } - } - public T[] toArray() { // Only generic way to create a T[] from another T[] T[] out = Arrays.copyOf(mBuffer, size(), (Class<T[]>) mBuffer.getClass()); diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp index 5b95ee79f330..f6fe3dd842da 100644 --- a/core/jni/android_hardware_OverlayProperties.cpp +++ b/core/jni/android_hardware_OverlayProperties.cpp @@ -61,13 +61,12 @@ static jboolean android_hardware_OverlayProperties_supportFp16ForHdr(JNIEnv* env static_cast<int32_t>(HAL_PIXEL_FORMAT_RGBA_FP16)) != i.pixelFormats.end() && std::find(i.standards.begin(), i.standards.end(), - static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT2020)) != + static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT709)) != i.standards.end() && std::find(i.transfers.begin(), i.transfers.end(), - static_cast<int32_t>(HAL_DATASPACE_TRANSFER_ST2084)) != - i.transfers.end() && + static_cast<int32_t>(HAL_DATASPACE_TRANSFER_SRGB)) != i.transfers.end() && std::find(i.ranges.begin(), i.ranges.end(), - static_cast<int32_t>(HAL_DATASPACE_RANGE_FULL)) != i.ranges.end()) { + static_cast<int32_t>(HAL_DATASPACE_RANGE_EXTENDED)) != i.ranges.end()) { return true; } } diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 55326da2c7df..25b2aaf3d737 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -1938,11 +1938,11 @@ static void nativeSetFrameTimelineVsync(JNIEnv* env, jclass clazz, jlong transac transaction->setFrameTimelineInfo(ftInfo); } -static void nativeSetDesiredPresentTime(JNIEnv* env, jclass clazz, jlong transactionObj, - jlong desiredPresentTime) { +static void nativeSetDesiredPresentTimeNanos(JNIEnv* env, jclass clazz, jlong transactionObj, + jlong desiredPresentTimeNanos) { auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); - transaction->setDesiredPresentTime(desiredPresentTime); + transaction->setDesiredPresentTime(desiredPresentTimeNanos); } static void nativeAddTransactionCommittedListener(JNIEnv* env, jclass clazz, jlong transactionObj, @@ -2412,8 +2412,8 @@ static const JNINativeMethod sSurfaceControlMethods[] = { {"getNativeTrustedPresentationCallbackFinalizer", "()J", (void*)getNativeTrustedPresentationCallbackFinalizer }, {"nativeGetStalledTransactionInfo", "(I)Landroid/gui/StalledTransactionInfo;", (void*) nativeGetStalledTransactionInfo }, - {"nativeSetDesiredPresentTime", "(JJ)V", - (void*) nativeSetDesiredPresentTime }, + {"nativeSetDesiredPresentTimeNanos", "(JJ)V", + (void*) nativeSetDesiredPresentTimeNanos }, // clang-format on }; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5f3f6419418a..adea8000cebc 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3042,6 +3042,17 @@ <permission android:name="android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE" android:protectionLevel="internal|role" /> + <!-- Used to provide the Telecom framework with access to the last known call ID. + <p>Protection level: signature + @SystemApi + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") + @hide + --> + <permission android:name="android.permission.ACCESS_LAST_KNOWN_CELL_ID" + android:protectionLevel="signature" + android:label="@string/permlab_accessLastKnownCellId" + android:description="@string/permdesc_accessLastKnownCellId"/> + <!-- ================================== --> <!-- Permissions for sdcard interaction --> <!-- ================================== --> @@ -3160,7 +3171,7 @@ types of interactions @hide --> <permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" - android:protectionLevel="signature|installer|role" /> + android:protectionLevel="signature|installer|module|role" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <!-- Allows interaction across profiles in the same profile group. --> @@ -5055,7 +5066,7 @@ <p>Intended for use by ROLE_ASSISTANT and signature apps only. --> <permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" - android:protectionLevel="signature|role"/> + android:protectionLevel="signature|module|role"/> <!-- Must be required by a {@link android.service.autofill.AutofillService}, to ensure that only the system can bind to it. @@ -5673,7 +5684,7 @@ <!-- @SystemApi Allows an application to manage the holders of a role. @hide --> <permission android:name="android.permission.MANAGE_ROLE_HOLDERS" - android:protectionLevel="signature|installer" /> + android:protectionLevel="signature|installer|module" /> <!-- @SystemApi Allows an application to manage the holders of roles associated with default applications. @@ -5693,7 +5704,7 @@ <!-- @SystemApi Allows an application to observe role holder changes. @hide --> <permission android:name="android.permission.OBSERVE_ROLE_HOLDERS" - android:protectionLevel="signature|installer" /> + android:protectionLevel="signature|installer|module" /> <!-- Allows an application to manage the companion devices. @hide --> @@ -7896,6 +7907,26 @@ <permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" android:protectionLevel="signature|role" /> + <!-- @SystemApi + @FlaggedApi("android.app.bic_client") + Allows app to call BackgroundInstallControlManager API to retrieve silently installed apps + for all users on device. + <p>Apps with a BackgroundInstallControlManager client will not be able to call any API without + this permission. + <p>Protection level: signature|role + @hide + --> + <permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" + android:protectionLevel="signature|role" /> + + <!-- @SystemApi Allows an application to read the system grammatical gender. + @FlaggedApi("android.app.system_terms_of_address_enabled") + <p>Protection level: signature|privileged|appop + @hide + --> + <permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" + android:protectionLevel="signature|privileged|appop"/> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml index f3acab063bbf..fe12f6ee7836 100644 --- a/core/res/res/drawable-nodpi/platlogo.xml +++ b/core/res/res/drawable-nodpi/platlogo.xml @@ -1,5 +1,5 @@ <!-- -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. @@ -20,179 +20,103 @@ Copyright (C) 2021 The Android Open Source Project android:viewportWidth="512" android:viewportHeight="512"> <path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"> + android:pathData="M256.22,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98s-24.52,-54.37 -11.33,-77.21c13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21s-103.26,178.86 -120.65,208.98c-17.39,30.12 -34.83,48.42 -61.2,48.42Z" + android:strokeWidth="0"> <aapt:attr name="android:fillColor"> <gradient - android:startX="256" - android:startY="21.81" - android:endX="256" - android:endY="350.42" + android:startX="56.22" + android:startY="256" + android:endX="456.22" + android:endY="256" android:type="linear"> <item android:offset="0" android:color="#FF073042"/> <item android:offset="1" android:color="#FF073042"/> </gradient> </aapt:attr> </path> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="m195.27,187.64l2.25,-6.69c13.91,78.13 50.84,284.39 50.84,50.33 0,-0.97 0.72,-1.81 1.62,-1.81h12.69c0.9,0 1.62,0.83 1.62,1.8 -0.2,409.91 -69.03,-43.64 -69.03,-43.64Z" - android:fillColor="#3ddc84"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="m158.77,180.68l-33.17,57.45c-1.9,3.3 -0.77,7.52 2.53,9.42 3.3,1.9 7.52,0.77 9.42,-2.53l33.59,-58.17c54.27,24.33 116.34,24.33 170.61,0l33.59,58.17c1.97,3.26 6.21,4.3 9.47,2.33 3.17,-1.91 4.26,-5.99 2.47,-9.23l-33.16,-57.45c56.95,-30.97 95.91,-88.64 101.61,-156.76H57.17c5.7,68.12 44.65,125.79 101.61,156.76Z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M250.26,187.66L262.17,187.66A2.13,2.13 0,0 1,264.3 189.78L264.3,217.85A2.13,2.13 0,0 1,262.17 219.98L250.26,219.98A2.13,2.13 0,0 1,248.14 217.85L248.14,189.78A2.13,2.13 0,0 1,250.26 187.66z" - android:fillColor="#3ddc84"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M250.12,170.29L262.32,170.29A1.98,1.98 0,0 1,264.3 172.26L264.3,176.39A1.98,1.98 0,0 1,262.32 178.37L250.12,178.37A1.98,1.98 0,0 1,248.14 176.39L248.14,172.26A1.98,1.98 0,0 1,250.12 170.29z" - android:fillColor="#3ddc84"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M171.92,216.82h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M188.8,275.73h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M369.04,337.63h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M285.93,252.22h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M318.96,218.84h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M294.05,288.55h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M330.82,273.31h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M188.8,298.95h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M220.14,238.94h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M272.1,318.9h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M293.34,349.25h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M161.05,254.24h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M378.92,192h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M137.87,323.7h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0" - android:strokeWidth="56.561" - android:fillColor="#00000000" - android:strokeColor="#f86734"/> <path - android:pathData="m256.22,126.57c-6.69,0 -12.12,5.27 -12.12,11.77v14.52c0,1.25 1.02,2.27 2.27,2.27h0c1.25,0 2.27,-1.02 2.27,-2.27v-3.91c0,-2.51 2.04,-4.55 4.55,-4.55h6.06c2.51,0 4.55,2.04 4.55,4.55v3.91c0,1.25 1.02,2.27 2.27,2.27s2.27,-1.02 2.27,-2.27v-14.52c0,-6.5 -5.43,-11.77 -12.12,-11.77Z" + android:pathData="M198.85,168.57l2.03,-6.03c12.55,70.48 45.87,256.56 45.87,45.41 0,-0.88 0.65,-1.63 1.46,-1.63h11.45c0.81,0 1.46,0.75 1.46,1.63 -0.18,369.8 -62.28,-39.37 -62.28,-39.37Z" + android:strokeWidth="0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M186.69,167.97l-23.69,41.03c-1.36,2.36 -0.55,5.37 1.8,6.73 2.36,1.36 5.37,0.55 6.73,-1.8l23.99,-41.55c38.76,17.38 83.1,17.38 121.86,0l23.99,41.55c1.41,2.33 4.44,3.07 6.77,1.66 2.26,-1.37 3.04,-4.28 1.76,-6.59l-23.69,-41.03c40.68,-22.12 68.5,-63.31 72.57,-111.97H114.11c4.07,48.65 31.89,89.85 72.57,111.97Z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M248.46,168.59L259.2,168.59A1.92,1.92 0,0 1,261.12 170.5L261.12,195.83A1.92,1.92 0,0 1,259.2 197.75L248.46,197.75A1.92,1.92 0,0 1,246.54 195.83L246.54,170.5A1.92,1.92 0,0 1,248.46 168.59z" + android:strokeWidth="0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M248.32,152.92L259.34,152.92A1.78,1.78 0,0 1,261.12 154.7L261.12,158.43A1.78,1.78 0,0 1,259.34 160.21L248.32,160.21A1.78,1.78 0,0 1,246.54 158.43L246.54,154.7A1.78,1.78 0,0 1,248.32 152.92z" + android:strokeWidth="0" android:fillColor="#3ddc84"/> <path - android:pathData="m93.34,116.36l3.85,-4.36 29.64,9.76 -4.44,5.03 -6.23,-2.1 -7.86,8.91 2.86,5.92 -4.43,5.03 -13.39,-28.18ZM110.43,122.76l-8.86,-3.02 4.11,8.41 4.76,-5.39Z" + android:pathData="M159.03,176.91h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M188.8,275.73h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M373.41,158.93h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M112.1,129.34h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M285.93,252.22h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M318.96,218.84h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M294.05,288.55h4.04v4.04h-4.04z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m153.62,100.85l-21.71,-6.2 10.38,14.38 -5.21,3.76 -16.78,-23.26 4.49,-3.24 21.65,6.19 -10.35,-14.35 5.24,-3.78 16.78,23.26 -4.49,3.24Z" + android:pathData="M330.82,263.31h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m161.46,63.15l8.99,-3.84c7.43,-3.18 15.96,0.12 19.09,7.44 3.13,7.32 -0.38,15.76 -7.81,18.94l-8.99,3.84 -11.28,-26.38ZM179.41,80.26c4.46,-1.91 5.96,-6.72 4.15,-10.96 -1.81,-4.24 -6.33,-6.48 -10.79,-4.57l-3.08,1.32 6.64,15.53 3.08,-1.32Z" + android:pathData="M188.8,298.95h4.04v4.04h-4.04z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m204.23,47.57l11.1,-2.2c5.31,-1.05 9.47,2.08 10.4,6.76 0.72,3.65 -0.76,6.37 -4.07,8.34l12.4,10.44 -7.57,1.5 -11.65,-9.76 -1.03,0.2 2.3,11.61 -6.3,1.25 -5.57,-28.14ZM216.78,56.7c1.86,-0.37 3,-1.71 2.68,-3.33 -0.34,-1.7 -1.88,-2.43 -3.74,-2.06l-4.04,0.8 1.07,5.39 4.04,-0.8Z" + android:pathData="M224.18,216.82h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m244.29,55.6c0.13,-8.16 6.86,-14.72 15.06,-14.58 8.16,0.13 14.72,6.9 14.58,15.06s-6.91,14.72 -15.06,14.58c-8.2,-0.13 -14.71,-6.9 -14.58,-15.06ZM267.44,55.98c0.08,-4.64 -3.54,-8.66 -8.18,-8.74 -4.68,-0.08 -8.42,3.82 -8.5,8.47 -0.08,4.65 3.54,8.66 8.22,8.74 4.64,0.08 8.39,-3.82 8.46,-8.47Z" + android:pathData="M272.1,318.9h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m294.39,44.84l6.31,1.23 -5.49,28.16 -6.31,-1.23 5.49,-28.16Z" + android:pathData="M293.34,339.25h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m321.94,51.41l9.14,3.48c7.55,2.88 11.39,11.17 8.56,18.61 -2.83,7.44 -11.22,11.07 -18.77,8.19l-9.14,-3.48 10.22,-26.8ZM322.96,76.19c4.53,1.73 8.95,-0.69 10.6,-5 1.64,-4.3 -0.05,-9.06 -4.58,-10.78l-3.13,-1.19 -6.01,15.78 3.13,1.19Z" + android:pathData="M165.09,236.28h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m381.41,89.24l-4.21,-3.21 3.65,-4.78 9.06,6.91 -17.4,22.81 -4.85,-3.7 13.75,-18.02Z" + android:pathData="M378.92,192h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m397.96,126.37l-9.56,-10.26 3.61,-3.36 22.8,-1.25 3.74,4.02 -12.35,11.51 2.51,2.69 -4.08,3.8 -2.51,-2.69 -4.55,4.24 -4.16,-4.46 4.55,-4.24ZM407.83,117.17l-10.28,0.58 4.49,4.82 5.79,-5.4Z" + android:pathData="M204.28,314.86h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> + <path + android:pathData="M253.83,118.47c-6.04,0 -10.93,4.76 -10.93,10.62v13.1c0,1.13 0.92,2.05 2.05,2.05h0c1.13,0 2.05,-0.92 2.05,-2.05v-3.53c0,-2.27 1.84,-4.1 4.1,-4.1h5.47c2.27,0 4.1,1.84 4.1,4.1v3.53c0,1.13 0.92,2.05 2.05,2.05s2.05,-0.92 2.05,-2.05v-13.1c0,-5.86 -4.9,-10.62 -10.93,-10.62Z" + android:strokeWidth="0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M256,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98 -17.39,-30.12 -24.52,-54.37 -11.33,-77.21 13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21 -17.39,30.12 -103.26,178.86 -120.65,208.98 -17.39,30.12 -34.83,48.42 -61.2,48.42Z" + android:strokeWidth="55" + android:fillColor="#00000000" + android:strokeColor="#f86733"/> </vector> - diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 5cfb1a3e44ba..2eb28eb555e8 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4372,6 +4372,14 @@ <attr name="name" /> </declare-styleable> + <!-- Specify one or more <code>polling-loop-filter</code> elements inside a + <code>host-apdu-service</code> to indicate polling loop frames that + your service can handle. --> + <declare-styleable name="PollingLoopFilter"> + <!-- The polling loop frame. This attribute is mandatory. --> + <attr name="name" /> + </declare-styleable> + <!-- Use <code>host-nfcf-service</code> as the root tag of the XML resource that describes an {@link android.nfc.cardemulation.HostNfcFService} service, which is referenced from its {@link android.nfc.cardemulation.HostNfcFService#SERVICE_META_DATA} diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 8fae6db4114a..29086a452ee6 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1813,6 +1813,12 @@ <attr name="allowUpdateOwnership" format="boolean" /> + <!-- This attribute can be applied to any tag in the manifest. The system uses its value to + determine whether the element (e.g., a permission) should be enabled or disabled + depending on the state of feature flags. + @hide @SystemApi @FlaggedApi("android.content.res.manifest_flagging") --> + <attr name="featureFlag" format="string" /> + <!-- The <code>manifest</code> tag is the root of an <code>AndroidManifest.xml</code> file, describing the contents of an Android package (.apk) file. One diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 28adccd6b0a1..5be29a6d68b8 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -57,7 +57,6 @@ <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item> <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item> <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item> - <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item> <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item> <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item> <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item> @@ -103,7 +102,6 @@ <string translatable="false" name="status_bar_call_strength">call_strength</string> <string translatable="false" name="status_bar_sensors_off">sensors_off</string> <string translatable="false" name="status_bar_screen_record">screen_record</string> - <string translatable="false" name="status_bar_oem_satellite">satellite</string> <!-- Flag indicating whether the surface flinger has limited alpha compositing functionality in hardware. If set, the window diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 53464547c272..d0216b308a4c 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -192,8 +192,13 @@ <!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC of the satellite PLMN with the format "mccmnc". --> - <string name="config_satellite_sim_identifier" translatable="false"></string> - <java-symbol type="string" name="config_satellite_sim_identifier" /> + <string name="config_satellite_sim_plmn_identifier" translatable="false"></string> + <java-symbol type="string" name="config_satellite_sim_plmn_identifier" /> + + <!-- The identifier for the satellite's SIM profile. The identifier is the service provider name + (spn) from the profile metadata. --> + <string name="config_satellite_sim_spn_identifier" translatable="false"></string> + <java-symbol type="string" name="config_satellite_sim_spn_identifier" /> <!-- The app to which the emergency call will be handed over for OEM-enabled satellite messaging. The format of the config string is "package_name;class_name". --> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 53b473e0ac1f..7d2288599841 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -119,6 +119,8 @@ <public name="optional"/> <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> <public name="adServiceTypes" /> + <!-- @hide @SystemApi @FlaggedApi("android.content.res.manifest_flagging") --> + <public name="featureFlag"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index f2858066b55b..bd3a5e4fe67c 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1075,6 +1075,12 @@ <string name="permdesc_manageOngoingCalls">Allows an app to see details about ongoing calls on your device and to control these calls.</string> + <!-- Title for an application permission, listed so that the user can access the last known cell id provided by telephony. --> + <string name="permlab_accessLastKnownCellId">Access last known cell identity.</string> + <!-- Description on an application permission, listed so that the user can access the last known cell id provided by telephony --> + <string name="permdesc_accessLastKnownCellId">Allows an app to access to the last known + cell identity provided by telephony.</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_readCellBroadcasts">read cell broadcast messages</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef12d8f4ac0f..d12ef2b95f06 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3198,7 +3198,6 @@ <java-symbol type="string" name="status_bar_camera" /> <java-symbol type="string" name="status_bar_sensors_off" /> <java-symbol type="string" name="status_bar_screen_record" /> - <java-symbol type="string" name="status_bar_oem_satellite" /> <!-- Locale picker --> <java-symbol type="id" name="locale_search_menu" /> diff --git a/core/tests/BroadcastRadioTests/TEST_MAPPING b/core/tests/BroadcastRadioTests/TEST_MAPPING new file mode 100644 index 000000000000..b085a27b25b8 --- /dev/null +++ b/core/tests/BroadcastRadioTests/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "BroadcastRadioTests" + } + ] +} diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java index b1cf9c2d4422..bb69fa42a63a 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramSelectorTest.java @@ -124,6 +124,17 @@ public final class ProgramSelectorTest { } @Test + public void construct_withNullInSecondaryIdsForSelector() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + new ProgramSelector(DAB_PROGRAM_TYPE, DAB_DMB_SID_EXT_IDENTIFIER_1, + new ProgramSelector.Identifier[]{null}, /* vendorIds= */ null); + }); + + assertWithMessage("Exception for null secondary id") + .that(thrown).hasMessageThat().contains("secondaryIds list must not contain nulls"); + } + + @Test public void getProgramType() { ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null); @@ -269,11 +280,11 @@ public final class ProgramSelectorTest { ProgramSelector selector = ProgramSelector.createAmFmSelector(band, (int) AM_FREQUENCY); - assertWithMessage("Program type") + assertWithMessage("AM program type without subchannel") .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM); - assertWithMessage("Primary identifiers") + assertWithMessage("AM primary identifiers without subchannel") .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected); - assertWithMessage("Secondary identifiers") + assertWithMessage("AM secondary identifiers without subchannel") .that(selector.getSecondaryIds()).isEmpty(); } @@ -285,15 +296,29 @@ public final class ProgramSelectorTest { ProgramSelector selector = ProgramSelector.createAmFmSelector( RadioManager.BAND_INVALID, (int) FM_FREQUENCY); - assertWithMessage("Program type") + assertWithMessage("FM program type without band and subchannel") .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_FM); - assertWithMessage("Primary identifiers") + assertWithMessage("FM primary identifiers without band and subchannel") .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected); - assertWithMessage("Secondary identifiers") + assertWithMessage("FM secondary identifiers without band and subchannel") .that(selector.getSecondaryIds()).isEmpty(); } @Test + public void createAmFmSelector_withValidFrequency() { + ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, AM_FREQUENCY); + + ProgramSelector selector = ProgramSelector.createAmFmSelector(RadioManager.BAND_INVALID, + (int) AM_FREQUENCY); + + assertWithMessage("AM program type with valid frequency") + .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM); + assertWithMessage("AM primary identifiers with valid frequency") + .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected); + } + + @Test public void createAmFmSelector_withValidFrequencyAndSubChannel() { int band = RadioManager.BAND_AM_HD; int subChannel = 2; @@ -307,15 +332,26 @@ public final class ProgramSelectorTest { ProgramSelector selector = ProgramSelector.createAmFmSelector(band, (int) AM_FREQUENCY, subChannel); - assertWithMessage("Program type") + assertWithMessage("AM program type with valid frequency and subchannel") .that(selector.getProgramType()).isEqualTo(ProgramSelector.PROGRAM_TYPE_AM); - assertWithMessage("Primary identifiers") + assertWithMessage("AM primary identifiers with valid frequency and subchannel") .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected); - assertWithMessage("Secondary identifiers") + assertWithMessage("AM secondary identifiers with valid frequency and subchannel") .that(selector.getSecondaryIds()).isEqualTo(secondaryIdExpected); } @Test + public void createAmFmSelector_withInvalidBand_throwsIllegalArgumentException() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + ProgramSelector.createAmFmSelector(/* band= */ 1000, (int) AM_FREQUENCY); + }); + + assertWithMessage("Exception for using invalid band") + .that(thrown).hasMessageThat().contains( + "Unknown band"); + } + + @Test public void createAmFmSelector_withInvalidFrequency_throwsIllegalArgumentException() { int invalidFrequency = 50000; diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java index 89464d14d1c7..bbac69f35e81 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java @@ -169,6 +169,16 @@ public final class RadioManagerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Test + public void constructor_withUnsupportedTypeForBandDescriptor_throwsException() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> new RadioManager.AmBandDescriptor(REGION, /* type= */ 100, AM_LOWER_LIMIT, + AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED)); + + assertWithMessage("Unsupported band type exception") + .that(thrown).hasMessageThat().contains("Unsupported band"); + } + + @Test public void getType_forBandDescriptor() { RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor(); @@ -363,6 +373,18 @@ public final class RadioManagerTest { } @Test + public void equals_withAmBandDescriptorsAndOtherTypeObject() { + assertWithMessage("AM Band Descriptor") + .that(AM_BAND_DESCRIPTOR).isNotEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void equals_withFmBandDescriptorsAndOtherTypeObject() { + assertWithMessage("FM Band Descriptor") + .that(FM_BAND_DESCRIPTOR).isNotEqualTo(AM_BAND_DESCRIPTOR); + } + + @Test public void equals_withAmBandDescriptorsOfDifferentUpperLimits_returnsFalse() { RadioManager.AmBandDescriptor amBandDescriptorCompared = new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT, @@ -373,9 +395,83 @@ public final class RadioManagerTest { } @Test - public void equals_withAmAndFmBandDescriptors_returnsFalse() { - assertWithMessage("AM Band Descriptor") - .that(AM_BAND_DESCRIPTOR).isNotEqualTo(FM_BAND_DESCRIPTOR); + public void equals_withAmBandDescriptorsOfDifferentStereoSupportValues() { + RadioManager.AmBandDescriptor amBandDescriptorCompared = + new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT, + AM_UPPER_LIMIT, AM_SPACING, !STEREO_SUPPORTED); + + assertWithMessage("AM Band Descriptor of different stereo support values") + .that(AM_BAND_DESCRIPTOR).isNotEqualTo(amBandDescriptorCompared); + } + + @Test + public void equals_withFmBandDescriptorsOfDifferentSpacingValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING * 2, + STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different support limit values") + .that(FM_BAND_DESCRIPTOR).isNotEqualTo(fmBandDescriptorCompared); + } + + @Test + public void equals_withFmBandConfigsOfDifferentLowerLimitValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION + 1, RadioManager.BAND_AM_HD, AM_LOWER_LIMIT, AM_UPPER_LIMIT, AM_SPACING, + STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different region values") + .that(FM_BAND_DESCRIPTOR).isNotEqualTo(fmBandDescriptorCompared); + } + + @Test + public void equals_withFmBandDescriptorsOfDifferentStereoSupportValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING, + !STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different stereo support values") + .that(fmBandDescriptorCompared).isNotEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void equals_withFmBandDescriptorsOfDifferentRdsSupportValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING, + STEREO_SUPPORTED, !RDS_SUPPORTED, TA_SUPPORTED, AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different rds support values") + .that(fmBandDescriptorCompared).isNotEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void equals_withFmBandDescriptorsOfDifferentTaSupportValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING, + STEREO_SUPPORTED, RDS_SUPPORTED, !TA_SUPPORTED, AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different ta support values") + .that(fmBandDescriptorCompared).isNotEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void equals_withFmBandDescriptorsOfDifferentAfSupportValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING, + STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, !AF_SUPPORTED, EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different af support values") + .that(fmBandDescriptorCompared).isNotEqualTo(FM_BAND_DESCRIPTOR); + } + + @Test + public void equals_withFmBandDescriptorsOfDifferentEaSupportValues() { + RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor( + REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING, + STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, AF_SUPPORTED, !EA_SUPPORTED); + + assertWithMessage("FM Band Descriptors of different ea support values") + .that(fmBandDescriptorCompared).isNotEqualTo(FM_BAND_DESCRIPTOR); } @Test @@ -587,18 +683,79 @@ public final class RadioManagerTest { } @Test - public void equals_withFmBandConfigsOfDifferentAfs_returnsFalse() { - RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder( - createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED) - .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED); - RadioManager.FmBandConfig fmBandConfigFromBuilder = builder.build(); + public void equals_withFmBandConfigsOfDifferentRegionValues() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION + 1, RadioManager.BAND_AM_HD, + AM_LOWER_LIMIT, AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, + TA_SUPPORTED, AF_SUPPORTED, EA_SUPPORTED)); + + assertWithMessage("FM Band Config of different regions") + .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigCompared); + } + + @Test + public void equals_withFmBandConfigsOfDifferentStereoSupportValues() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, !STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, + AF_SUPPORTED, EA_SUPPORTED)); + + assertWithMessage("FM Band Config with different stereo support values") + .that(fmBandConfigCompared).isNotEqualTo(FM_BAND_CONFIG); + } - assertWithMessage("FM Band Config of different af value") - .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigFromBuilder); + @Test + public void equals_withFmBandConfigsOfDifferentRdsSupportValues() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, !RDS_SUPPORTED, TA_SUPPORTED, + AF_SUPPORTED, EA_SUPPORTED)); + + assertWithMessage("FM Band Config with different RDS support values") + .that(fmBandConfigCompared).isNotEqualTo(FM_BAND_CONFIG); + } + + @Test + public void equals_withFmBandConfigsOfDifferentTaSupportValues() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, !TA_SUPPORTED, + AF_SUPPORTED, EA_SUPPORTED)); + + assertWithMessage("FM Band Configs with different ta values") + .that(fmBandConfigCompared).isNotEqualTo(FM_BAND_CONFIG); + } + + @Test + public void equals_withFmBandConfigsOfDifferentAfSupportValues() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, + !AF_SUPPORTED, EA_SUPPORTED)); + + assertWithMessage("FM Band Config of different af support value") + .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigCompared); + } + + @Test + public void equals_withFmBandConfigsOfDifferentEaSupportValues() { + RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( + new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, + FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, + AF_SUPPORTED, !EA_SUPPORTED)); + + assertWithMessage("FM Band Configs with different ea support values") + .that(fmBandConfigCompared).isNotEqualTo(FM_BAND_CONFIG); + } + + @Test + public void equals_withAmBandConfigsAndOtherTypeObject() { + assertWithMessage("AM Band Config") + .that(AM_BAND_CONFIG).isNotEqualTo(FM_BAND_CONFIG); } @Test - public void equals_withFmAndAmBandConfigs_returnsFalse() { + public void equals_withFmBandConfigsAndOtherTypeObject() { assertWithMessage("FM Band Config") .that(FM_BAND_CONFIG).isNotEqualTo(AM_BAND_CONFIG); } diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java index 7b9121eb6085..fddfd397d068 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java @@ -16,14 +16,20 @@ package android.hardware.radio; -import static com.google.common.truth.Truth.assertWithMessage; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; import android.graphics.Bitmap; import android.os.Parcel; import android.platform.test.flag.junit.SetFlagsRule; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; +import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase; + +import com.google.common.truth.Expect; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,7 +40,7 @@ import java.util.Arrays; import java.util.Set; @RunWith(MockitoJUnitRunner.class) -public final class RadioMetadataTest { +public final class RadioMetadataTest extends ExtendedRadioMockitoTestCase { private static final int CREATOR_ARRAY_SIZE = 3; private static final int INT_KEY_VALUE = 1; @@ -49,14 +55,21 @@ public final class RadioMetadataTest { private Bitmap mBitmapValue; @Rule + public final Expect mExpect = Expect.create(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Override + protected void initializeSession(StaticMockitoSessionBuilder builder) { + builder.spyStatic(Bitmap.class); + } + @Test public void describeContents_forClock() { RadioMetadata.Clock clock = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH, TEST_TIME_ZONE_OFFSET_MINUTES); - assertWithMessage("Describe contents for metadata clock") + mExpect.withMessage("Describe contents for metadata clock") .that(clock.describeContents()).isEqualTo(0); } @@ -64,7 +77,7 @@ public final class RadioMetadataTest { public void newArray_forClockCreator() { RadioMetadata.Clock[] clocks = RadioMetadata.Clock.CREATOR.newArray(CREATOR_ARRAY_SIZE); - assertWithMessage("Clock array size").that(clocks.length).isEqualTo(CREATOR_ARRAY_SIZE); + mExpect.withMessage("Clock array size").that(clocks.length).isEqualTo(CREATOR_ARRAY_SIZE); } @Test @@ -77,9 +90,9 @@ public final class RadioMetadataTest { parcel.setDataPosition(0); RadioMetadata.Clock clockFromParcel = RadioMetadata.Clock.CREATOR.createFromParcel(parcel); - assertWithMessage("UTC second since epoch of metadata clock created from parcel") + mExpect.withMessage("UTC second since epoch of metadata clock created from parcel") .that(clockFromParcel.getUtcEpochSeconds()).isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH); - assertWithMessage("Time zone offset minutes of metadata clock created from parcel") + mExpect.withMessage("Time zone offset minutes of metadata clock created from parcel") .that(clockFromParcel.getTimezoneOffsetMinutes()) .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES); } @@ -92,7 +105,7 @@ public final class RadioMetadataTest { mBuilder.putString(invalidStringKey, "value"); }); - assertWithMessage("Exception for putting illegal string-value key %s", invalidStringKey) + mExpect.withMessage("Exception for putting illegal string-value key %s", invalidStringKey) .that(thrown).hasMessageThat() .matches(".*" + invalidStringKey + ".*cannot.*String.*?"); } @@ -105,12 +118,25 @@ public final class RadioMetadataTest { mBuilder.putInt(invalidIntKey, INT_KEY_VALUE); }); - assertWithMessage("Exception for putting illegal int-value for key %s", invalidIntKey) + mExpect.withMessage("Exception for putting illegal int-value for key %s", invalidIntKey) .that(thrown).hasMessageThat() .matches(".*" + invalidIntKey + ".*cannot.*int.*?"); } @Test + public void putBitmap_withIllegalKey() { + String invalidIntKey = RadioMetadata.METADATA_KEY_GENRE; + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + mBuilder.putBitmap(invalidIntKey, mBitmapValue); + }); + + mExpect.withMessage("Exception for putting illegal bitmap-value for key %s", invalidIntKey) + .that(thrown).hasMessageThat() + .matches(".*" + invalidIntKey + ".*cannot.*Bitmap.*?"); + } + + @Test public void putClock_withIllegalKey() { String invalidClockKey = RadioMetadata.METADATA_KEY_ALBUM; @@ -119,7 +145,7 @@ public final class RadioMetadataTest { /* timezoneOffsetMinutes= */ 1); }); - assertWithMessage("Exception for putting illegal clock-value key %s", invalidClockKey) + mExpect.withMessage("Exception for putting illegal clock-value key %s", invalidClockKey) .that(thrown).hasMessageThat() .matches(".*" + invalidClockKey + ".*cannot.*Clock.*?"); } @@ -133,7 +159,7 @@ public final class RadioMetadataTest { mBuilder.putStringArray(invalidStringArrayKey, UFIDS_VALUE); }); - assertWithMessage("Exception for putting illegal string-array-value for key %s", + mExpect.withMessage("Exception for putting illegal string-array-value for key %s", invalidStringArrayKey).that(thrown).hasMessageThat() .matches(".*" + invalidStringArrayKey + ".*cannot.*Array.*?"); } @@ -146,7 +172,7 @@ public final class RadioMetadataTest { mBuilder.putStringArray(/* key= */ null, UFIDS_VALUE); }); - assertWithMessage("Exception for putting string-array with null key") + mExpect.withMessage("Exception for putting string-array with null key") .that(thrown).hasMessageThat().contains("can not be null"); } @@ -158,7 +184,7 @@ public final class RadioMetadataTest { mBuilder.putStringArray(RadioMetadata.METADATA_KEY_UFIDS, /* value= */ null); }); - assertWithMessage("Exception for putting null string-array") + mExpect.withMessage("Exception for putting null string-array") .that(thrown).hasMessageThat().contains("can not be null"); } @@ -167,7 +193,7 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_RDS_PI; RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build(); - assertWithMessage("Whether metadata contains %s in metadata", key) + mExpect.withMessage("Whether metadata contains %s in metadata", key) .that(metadata.containsKey(key)).isTrue(); } @@ -176,7 +202,7 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_RDS_PI; RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build(); - assertWithMessage("Whether metadata contains key %s not in metadata", key) + mExpect.withMessage("Whether metadata contains key %s not in metadata", key) .that(metadata.containsKey(RadioMetadata.METADATA_KEY_ARTIST)).isFalse(); } @@ -185,7 +211,7 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_RDS_PI; RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build(); - assertWithMessage("Int value for key %s in metadata", key) + mExpect.withMessage("Int value for key %s in metadata", key) .that(metadata.getInt(key)).isEqualTo(INT_KEY_VALUE); } @@ -195,7 +221,7 @@ public final class RadioMetadataTest { RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE).build(); - assertWithMessage("Int value for key %s in metadata", key) + mExpect.withMessage("Int value for key %s in metadata", key) .that(metadata.getInt(key)).isEqualTo(0); } @@ -204,7 +230,7 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_ARTIST; RadioMetadata metadata = mBuilder.putString(key, ARTIST_KEY_VALUE).build(); - assertWithMessage("String value for key %s in metadata", key) + mExpect.withMessage("String value for key %s in metadata", key) .that(metadata.getString(key)).isEqualTo(ARTIST_KEY_VALUE); } @@ -213,7 +239,7 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_ARTIST; RadioMetadata metadata = mBuilder.build(); - assertWithMessage("String value for key %s not in metadata", key) + mExpect.withMessage("String value for key %s not in metadata", key) .that(metadata.getString(key)).isNull(); } @@ -222,7 +248,7 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_ICON; RadioMetadata metadata = mBuilder.putBitmap(key, mBitmapValue).build(); - assertWithMessage("Bitmap value for key %s in metadata", key) + mExpect.withMessage("Bitmap value for key %s in metadata", key) .that(metadata.getBitmap(key)).isEqualTo(mBitmapValue); } @@ -231,16 +257,26 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_ICON; RadioMetadata metadata = mBuilder.build(); - assertWithMessage("Bitmap value for key %s not in metadata", key) + mExpect.withMessage("Bitmap value for key %s not in metadata", key) .that(metadata.getBitmap(key)).isNull(); } @Test + public void getBitmap_withIllegalKey() { + String illegalKey = RadioMetadata.METADATA_KEY_ARTIST; + RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ICON, INT_KEY_VALUE) + .build(); + + mExpect.withMessage("Bitmap id value with non-bitmap-type key %s", illegalKey) + .that(metadata.getBitmap(illegalKey)).isNull(); + } + + @Test public void getBitmapId_withKeyInMetadata() { String key = RadioMetadata.METADATA_KEY_ART; RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build(); - assertWithMessage("Bitmap id value for key %s in metadata", key) + mExpect.withMessage("Bitmap id value for key %s in metadata", key) .that(metadata.getBitmapId(key)).isEqualTo(INT_KEY_VALUE); } @@ -249,11 +285,21 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_ART; RadioMetadata metadata = mBuilder.build(); - assertWithMessage("Bitmap id value for key %s not in metadata", key) + mExpect.withMessage("Bitmap id value for key %s not in metadata", key) .that(metadata.getBitmapId(key)).isEqualTo(0); } @Test + public void getBitmapId_withIllegalKey() { + String illegalKey = RadioMetadata.METADATA_KEY_ARTIST; + RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ART, INT_KEY_VALUE) + .build(); + + mExpect.withMessage("Bitmap id value with non-bitmap-id-type key %s", illegalKey) + .that(metadata.getBitmapId(illegalKey)).isEqualTo(0); + } + + @Test public void getClock_withKeyInMetadata() { String key = RadioMetadata.METADATA_KEY_CLOCK; RadioMetadata metadata = mBuilder @@ -262,10 +308,10 @@ public final class RadioMetadataTest { RadioMetadata.Clock clockExpected = metadata.getClock(key); - assertWithMessage("Number of seconds since epoch of value for key %s in metadata", key) + mExpect.withMessage("Number of seconds since epoch of value for key %s in metadata", key) .that(clockExpected.getUtcEpochSeconds()) .isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH); - assertWithMessage("Offset of timezone in minutes of value for key %s in metadata", key) + mExpect.withMessage("Offset of timezone in minutes of value for key %s in metadata", key) .that(clockExpected.getTimezoneOffsetMinutes()) .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES); } @@ -275,17 +321,27 @@ public final class RadioMetadataTest { String key = RadioMetadata.METADATA_KEY_CLOCK; RadioMetadata metadata = mBuilder.build(); - assertWithMessage("Clock value for key %s not in metadata", key) + mExpect.withMessage("Clock value for key %s not in metadata", key) .that(metadata.getClock(key)).isNull(); } @Test + public void getClock_withIllegalKey() { + String illegalKey = RadioMetadata.METADATA_KEY_ARTIST; + RadioMetadata metadata = mBuilder.putClock(RadioMetadata.METADATA_KEY_CLOCK, + TEST_UTC_SECOND_SINCE_EPOCH, TEST_TIME_ZONE_OFFSET_MINUTES).build(); + + mExpect.withMessage("Clock value for non-clock-type key %s", illegalKey) + .that(metadata.getClock(illegalKey)).isNull(); + } + + @Test public void getStringArray_withKeyInMetadata() { mSetFlagsRule.enableFlags(Flags.FLAG_HD_RADIO_IMPROVED); String key = RadioMetadata.METADATA_KEY_UFIDS; RadioMetadata metadata = mBuilder.putStringArray(key, UFIDS_VALUE).build(); - assertWithMessage("String-array value for key %s not in metadata", key) + mExpect.withMessage("String-array value for key %s not in metadata", key) .that(metadata.getStringArray(key)).asList().isEqualTo(Arrays.asList(UFIDS_VALUE)); } @@ -299,7 +355,7 @@ public final class RadioMetadataTest { metadata.getStringArray(key); }); - assertWithMessage("Exception for getting string array for string-array value for key %s " + mExpect.withMessage("Exception for getting string array for string-array value for key %s " + "not in metadata", key).that(thrown).hasMessageThat().contains("not found"); } @@ -312,7 +368,7 @@ public final class RadioMetadataTest { metadata.getStringArray(/* key= */ null); }); - assertWithMessage("Exception for getting string array with null key") + mExpect.withMessage("Exception for getting string array with null key") .that(thrown).hasMessageThat().contains("can not be null"); } @@ -326,7 +382,7 @@ public final class RadioMetadataTest { metadata.getStringArray(invalidClockKey); }); - assertWithMessage("Exception for getting string array for key %s not of string-array type", + mExpect.withMessage("Exception for getting string array for non-string-array type key %s", invalidClockKey).that(thrown).hasMessageThat() .contains("string array"); } @@ -338,7 +394,7 @@ public final class RadioMetadataTest { .putString(RadioMetadata.METADATA_KEY_ARTIST, ARTIST_KEY_VALUE) .build(); - assertWithMessage("Size of fields in non-empty metadata") + mExpect.withMessage("Size of fields in non-empty metadata") .that(metadata.size()).isEqualTo(2); } @@ -346,7 +402,7 @@ public final class RadioMetadataTest { public void size_withEmptyMetadata() { RadioMetadata metadata = mBuilder.build(); - assertWithMessage("Size of fields in empty metadata") + mExpect.withMessage("Size of fields in empty metadata") .that(metadata.size()).isEqualTo(0); } @@ -360,7 +416,7 @@ public final class RadioMetadataTest { Set<String> metadataSet = metadata.keySet(); - assertWithMessage("Metadata set of non-empty metadata") + mExpect.withMessage("Metadata set of non-empty metadata") .that(metadataSet).containsExactly(RadioMetadata.METADATA_KEY_ICON, RadioMetadata.METADATA_KEY_RDS_PI, RadioMetadata.METADATA_KEY_ARTIST); } @@ -371,7 +427,7 @@ public final class RadioMetadataTest { Set<String> metadataSet = metadata.keySet(); - assertWithMessage("Metadata set of empty metadata") + mExpect.withMessage("Metadata set of empty metadata") .that(metadataSet).isEmpty(); } @@ -380,7 +436,7 @@ public final class RadioMetadataTest { int nativeKey = 0; String key = RadioMetadata.getKeyFromNativeKey(nativeKey); - assertWithMessage("Key for native key %s", nativeKey) + mExpect.withMessage("Key for native key %s", nativeKey) .that(key).isEqualTo(RadioMetadata.METADATA_KEY_RDS_PI); } @@ -393,7 +449,7 @@ public final class RadioMetadataTest { RadioMetadata.Builder copyBuilder = new RadioMetadata.Builder(metadata); RadioMetadata metadataCopied = copyBuilder.build(); - assertWithMessage("Metadata with the same contents") + mExpect.withMessage("Metadata with the same contents") .that(metadataCopied).isEqualTo(metadata); } @@ -401,14 +457,15 @@ public final class RadioMetadataTest { public void describeContents_forMetadata() { RadioMetadata metadata = mBuilder.build(); - assertWithMessage("Metadata contents").that(metadata.describeContents()).isEqualTo(0); + mExpect.withMessage("Metadata contents").that(metadata.describeContents()).isEqualTo(0); } @Test public void newArray_forRadioMetadataCreator() { RadioMetadata[] metadataArray = RadioMetadata.CREATOR.newArray(CREATOR_ARRAY_SIZE); - assertWithMessage("Radio metadata array").that(metadataArray).hasLength(CREATOR_ARRAY_SIZE); + mExpect.withMessage("Radio metadata array").that(metadataArray) + .hasLength(CREATOR_ARRAY_SIZE); } @Test @@ -423,7 +480,7 @@ public final class RadioMetadataTest { parcel.setDataPosition(0); RadioMetadata metadataFromParcel = RadioMetadata.CREATOR.createFromParcel(parcel); - assertWithMessage("Radio metadata created from parcel") + mExpect.withMessage("Radio metadata created from parcel") .that(metadataFromParcel).isEqualTo(metadataExpected); } @@ -441,7 +498,43 @@ public final class RadioMetadataTest { parcel.setDataPosition(0); RadioMetadata metadataFromParcel = RadioMetadata.CREATOR.createFromParcel(parcel); - assertWithMessage("Radio metadata created from parcel with string array type metadata") + mExpect.withMessage("Radio metadata created from parcel with string array type metadata") .that(metadataFromParcel).isEqualTo(metadataExpected); } + + @Test + public void build_withRadioMetadataMeetingSizeRequirement() { + int maxBitmapSize = 10; + doReturn(maxBitmapSize - 1).when(mBitmapValue).getHeight(); + doReturn(maxBitmapSize - 1).when(mBitmapValue).getWidth(); + RadioMetadata metadataSource = mBuilder.putBitmap( + RadioMetadata.METADATA_KEY_ICON, mBitmapValue).build(); + + RadioMetadata metadata = new RadioMetadata.Builder(metadataSource, maxBitmapSize).build(); + + mExpect.withMessage("Bitmap without resizing") + .that(metadata.getBitmap(RadioMetadata.METADATA_KEY_ICON)).isEqualTo(mBitmapValue); + } + + @Test + public void build_withRadioMetadataAboveSizeRequirement() { + int maxBitmapSize = 10; + int bitmapHeight = 10; + int bitmapWidth = 20; + int bitmapWidthResized = maxBitmapSize; + int bitmapHeightResized = (bitmapHeight * bitmapWidthResized) / bitmapWidth; + Bitmap bitmapResized = mock(Bitmap.class); + doReturn(bitmapHeight).when(mBitmapValue).getHeight(); + doReturn(bitmapWidth).when(mBitmapValue).getWidth(); + doReturn(bitmapResized).when(() -> Bitmap.createScaledBitmap( + mBitmapValue, bitmapWidthResized, bitmapHeightResized, /* filter= */ true)); + RadioMetadata metadataSource = mBuilder.putBitmap( + RadioMetadata.METADATA_KEY_ICON, mBitmapValue).build(); + + RadioMetadata metadata = new RadioMetadata.Builder(metadataSource, maxBitmapSize).build(); + + mExpect.withMessage("Bitmap with resizing") + .that(metadata.getBitmap(RadioMetadata.METADATA_KEY_ICON)) + .isEqualTo(bitmapResized); + } } diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java index b36367b91803..e68a0b8225e6 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/UniqueProgramIdentifierTest.java @@ -16,6 +16,8 @@ package android.hardware.radio; +import static org.junit.Assert.assertThrows; + import android.annotation.Nullable; import android.os.Parcel; @@ -45,6 +47,24 @@ public final class UniqueProgramIdentifierTest { public final Expect expect = Expect.create(); @Test + public void requireCriticalSecondaryIds_forDab() { + expect.withMessage("Critical secondary Id required for DAB") + .that(UniqueProgramIdentifier.requireCriticalSecondaryIds( + ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT)).isTrue(); + } + + @Test + public void constructor_withNullSelector() { + ProgramSelector nullSelector = null; + + NullPointerException thrown = assertThrows(NullPointerException.class, + () -> new UniqueProgramIdentifier(nullSelector)); + + expect.withMessage("Null pointer exception for unique program identifier") + .that(thrown).hasMessageThat().contains("can not be null"); + } + + @Test public void getPrimaryId_forUniqueProgramIdentifier() { ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null); @@ -67,6 +87,27 @@ public final class UniqueProgramIdentifierTest { } @Test + public void getCriticalSecondaryIds_forDabUniqueProgramIdentifierWithoutEnsemble() { + ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{ + DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null); + UniqueProgramIdentifier dabIdentifier = new UniqueProgramIdentifier(dabSelector); + + expect.withMessage("Critical secondary ids of DAB unique identifier without ensemble") + .that(dabIdentifier.getCriticalSecondaryIds()) + .containsExactly(DAB_FREQUENCY_IDENTIFIER); + } + + @Test + public void getCriticalSecondaryIds_forDabUniqueProgramIdentifierWithoutSecondaryIds() { + ProgramSelector dabSelector = getDabSelector(new ProgramSelector.Identifier[]{}, + /* vendorIds= */ null); + UniqueProgramIdentifier dabIdentifier = new UniqueProgramIdentifier(dabSelector); + + expect.withMessage("Critical secondary ids of DAB unique identifier") + .that(dabIdentifier.getCriticalSecondaryIds()).isEmpty(); + } + + @Test public void getCriticalSecondaryIds_forFmUniqueProgramIdentifier() { UniqueProgramIdentifier fmUniqueIdentifier = new UniqueProgramIdentifier( new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, @@ -147,6 +188,19 @@ public final class UniqueProgramIdentifierTest { } @Test + public void equals_withMissingSecondaryIdsForUniqueProgramIdentifier() { + ProgramSelector dabSelector1 = getDabSelector(new ProgramSelector.Identifier[]{ + DAB_ENSEMBLE_IDENTIFIER}, /* vendorIds= */ null); + UniqueProgramIdentifier dabIdentifier1 = new UniqueProgramIdentifier(dabSelector1); + ProgramSelector dabSelector2 = getDabSelector(new ProgramSelector.Identifier[]{ + DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER}, /* vendorIds= */ null); + UniqueProgramIdentifier dabIdentifier2 = new UniqueProgramIdentifier(dabSelector2); + + expect.withMessage("DAB unique identifier with missing secondary ids") + .that(dabIdentifier1).isNotEqualTo(dabIdentifier2); + } + + @Test public void describeContents_forUniqueProgramIdentifier() { UniqueProgramIdentifier fmUniqueIdentifier = new UniqueProgramIdentifier(FM_IDENTIFIER); diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index e18de2e66399..d1a90aea835c 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -62,6 +62,7 @@ android_test { "frameworks-core-util-lib", "mockwebserver", "guava", + "android.app.usage.flags-aconfig-java", "android.view.accessibility.flags-aconfig-java", "androidx.core_core", "androidx.core_core-ktx", diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java index 9d85b65d4dac..1925588e8904 100644 --- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java +++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java @@ -16,8 +16,6 @@ package android.app; -import static com.google.common.truth.Truth.assertThat; - import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; @@ -28,8 +26,6 @@ import android.net.Uri; import android.os.Parcel; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.service.notification.ZenDeviceEffects; -import android.service.notification.ZenPolicy; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -230,66 +226,4 @@ public class AutomaticZenRuleTest { assertThrows(IllegalArgumentException.class, () -> builder.setType(100)); } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testCanUpdate_nullPolicyAndDeviceEffects() { - AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name", - Uri.parse("uri://short")); - - AutomaticZenRule rule = builder.setUserModifiedFields(0) - .setZenPolicy(null) - .setDeviceEffects(null) - .build(); - - assertThat(rule.canUpdate()).isTrue(); - - rule = builder.setUserModifiedFields(1).build(); - assertThat(rule.canUpdate()).isFalse(); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testCanUpdate_policyModified() { - ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0); - ZenPolicy policy = policyBuilder.build(); - - AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name", - Uri.parse("uri://short")); - AutomaticZenRule rule = builder.setUserModifiedFields(0) - .setZenPolicy(policy) - .setDeviceEffects(null).build(); - - // Newly created ZenPolicy is not user modified. - assertThat(policy.getUserModifiedFields()).isEqualTo(0); - assertThat(rule.canUpdate()).isTrue(); - - policy = policyBuilder.setUserModifiedFields(1).build(); - assertThat(policy.getUserModifiedFields()).isEqualTo(1); - rule = builder.setZenPolicy(policy).build(); - assertThat(rule.canUpdate()).isFalse(); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testCanUpdate_deviceEffectsModified() { - ZenDeviceEffects.Builder deviceEffectsBuilder = - new ZenDeviceEffects.Builder().setUserModifiedFields(0); - ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); - - AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name", - Uri.parse("uri://short")); - AutomaticZenRule rule = builder.setUserModifiedFields(0) - .setZenPolicy(null) - .setDeviceEffects(deviceEffects).build(); - - // Newly created ZenDeviceEffects is not user modified. - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(0); - assertThat(rule.canUpdate()).isTrue(); - - deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build(); - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1); - rule = builder.setDeviceEffects(deviceEffects).build(); - assertThat(rule.canUpdate()).isFalse(); - } } diff --git a/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java b/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java index 839b645cf352..4565978434ee 100644 --- a/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java +++ b/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java @@ -15,24 +15,34 @@ */ package android.app.usage; +import static android.app.usage.Flags.FLAG_FILTER_BASED_EVENT_QUERY_API; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import android.app.usage.UsageEvents.Event; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Random; -import java.util.Set; @RunWith(AndroidJUnit4.class) @SmallTest public class UsageEventsQueryTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Test + @RequiresFlagsEnabled(FLAG_FILTER_BASED_EVENT_QUERY_API) public void testQueryDuration() { // Test with negative beginTimeMillis. long beginTimeMillis = -100; @@ -97,6 +107,7 @@ public class UsageEventsQueryTest { } @Test + @RequiresFlagsEnabled(FLAG_FILTER_BASED_EVENT_QUERY_API) public void testQueryEventTypes() { Random rnd = new Random(); UsageEventsQuery.Builder queryBuilder = new UsageEventsQuery.Builder(1000, 2000); @@ -104,7 +115,7 @@ public class UsageEventsQueryTest { // Test with invalid event type. int eventType = Event.NONE - 1; try { - queryBuilder.addEventTypes(eventType); + queryBuilder.setEventTypes(eventType); fail("Invalid event type: " + eventType); } catch (IllegalArgumentException e) { // Expected, fall through. @@ -112,7 +123,7 @@ public class UsageEventsQueryTest { eventType = Event.MAX_EVENT_TYPE + 1; try { - queryBuilder.addEventTypes(eventType); + queryBuilder.setEventTypes(eventType); fail("Invalid event type: " + eventType); } catch (IllegalArgumentException e) { // Expected, fall through. @@ -121,11 +132,11 @@ public class UsageEventsQueryTest { // Test with valid and duplicate event types. eventType = rnd.nextInt(Event.MAX_EVENT_TYPE + 1); try { - UsageEventsQuery query = queryBuilder.addEventTypes(eventType, eventType, eventType) + UsageEventsQuery query = queryBuilder.setEventTypes(eventType, eventType, eventType) .build(); - Set<Integer> eventTypeSet = query.getEventTypes(); - assertEquals(eventTypeSet.size(), 1); - int type = eventTypeSet.iterator().next(); + final int[] eventTypesArray = query.getEventTypes(); + assertEquals(eventTypesArray.length, 1); + int type = eventTypesArray[0]; assertEquals(type, eventType); } catch (IllegalArgumentException e) { fail("Valid event type: " + eventType); diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index 1617eda6a77c..e32a57b1aefe 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -48,7 +48,7 @@ class FontScaleConverterFactoryTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() - private lateinit var defaultLookupTables: SparseArray<FontScaleConverter> + private var defaultLookupTables: SparseArray<FontScaleConverter>? = null @Before fun setup() { @@ -58,7 +58,9 @@ class FontScaleConverterFactoryTest { @After fun teardown() { // Restore the default tables (since some tests will have added extras to the cache) - FontScaleConverterFactory.sLookupTables = defaultLookupTables + if (defaultLookupTables != null) { + FontScaleConverterFactory.sLookupTables = defaultLookupTables!! + } } @Test diff --git a/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt new file mode 100644 index 000000000000..27869bbf5392 --- /dev/null +++ b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt @@ -0,0 +1,224 @@ +/* + * 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 android.text + +import android.graphics.Paint +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +const val LEFT_EDGE = Paint.TEXT_RUN_FLAG_LEFT_EDGE +const val RIGHT_EDGE = Paint.TEXT_RUN_FLAG_RIGHT_EDGE +const val MIDDLE_OF_LINE = 0 +const val WHOLE_LINE = LEFT_EDGE or RIGHT_EDGE + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TextLineLetterSpacingTest { + + @Rule + @JvmField + val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION) + @Test + fun calculateRunFlagTest() { + // Only one Bidi run + assertThat(TextLine.calculateRunFlag(0, 1, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(WHOLE_LINE) + assertThat(TextLine.calculateRunFlag(0, 1, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(WHOLE_LINE) + + // Two BiDi Runs. + // If the layout is LTR, the first run is the left most run. + assertThat(TextLine.calculateRunFlag(0, 2, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(LEFT_EDGE) + // If the layout is LTR, the last run is the right most run. + assertThat(TextLine.calculateRunFlag(1, 2, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(RIGHT_EDGE) + // If the layout is RTL, the first run is the right most run. + assertThat(TextLine.calculateRunFlag(0, 2, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(RIGHT_EDGE) + // If the layout is RTL, the last run is the left most run. + assertThat(TextLine.calculateRunFlag(1, 2, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(LEFT_EDGE) + + // Three BiDi Runs. + // If the layout is LTR, the first run is the left most run. + assertThat(TextLine.calculateRunFlag(0, 3, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(LEFT_EDGE) + // Regardless of the context direction, the middle run must not have any flags. + assertThat(TextLine.calculateRunFlag(1, 3, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(MIDDLE_OF_LINE) + // If the layout is LTR, the last run is the right most run. + assertThat(TextLine.calculateRunFlag(2, 3, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(RIGHT_EDGE) + // If the layout is RTL, the first run is the right most run. + assertThat(TextLine.calculateRunFlag(0, 3, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(RIGHT_EDGE) + // Regardless of the context direction, the middle run must not have any flags. + assertThat(TextLine.calculateRunFlag(1, 3, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(MIDDLE_OF_LINE) + // If the layout is RTL, the last run is the left most run. + assertThat(TextLine.calculateRunFlag(2, 3, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(LEFT_EDGE) + } + + @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION) + @Test + fun resolveRunFlagForSubSequenceTest() { + val runStart = 5 + val runEnd = 15 + // Regardless of the run directions, if the span covers entire Bidi run, the same runFlag + // should be returned. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(LEFT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(LEFT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(RIGHT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(RIGHT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(WHOLE_LINE) + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(WHOLE_LINE) + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + + + + // Left edge of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(LEFT_EDGE) + // Left edge of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(RIGHT_EDGE) + // Whole line of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(LEFT_EDGE) + // Whole line of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(RIGHT_EDGE) + // Middle of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + + + + // Left edge of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + // Left edge of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(LEFT_EDGE) + // Right edge of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(RIGHT_EDGE) + // Right edge of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + // Whole line of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(RIGHT_EDGE) + // Whole line of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(LEFT_EDGE) + // Middle of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + + + + // Left edge of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Left edge of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Whole line of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Whole line of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index cf3eb12498ca..cfbda84f24e1 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -19,6 +19,7 @@ package android.view; import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; @@ -575,8 +576,13 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); + viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_HIGH_HINT); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW); diff --git a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java index 0742052cce53..ec4c563e469e 100644 --- a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java +++ b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java @@ -18,39 +18,67 @@ package com.android.internal.os; import static com.google.common.truth.Truth.assertThat; -import android.util.Xml; - import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; @RunWith(AndroidJUnit4.class) @SmallTest public class MonotonicClockTest { private final MockClock mClock = new MockClock(); + private File mFile; + + @Before + public void setup() throws IOException { + File systemDir = Files.createTempDirectory("MonotonicClockTest").toFile(); + mFile = new File(systemDir, "test_monotonic_clock.xml"); + if (mFile.exists()) { + assertThat(mFile.delete()).isTrue(); + } + } @Test public void persistence() throws IOException { - MonotonicClock monotonicClock = new MonotonicClock(1000, mClock); + MonotonicClock monotonicClock = new MonotonicClock(mFile, 1000, mClock); mClock.realtime = 234; assertThat(monotonicClock.monotonicTime()).isEqualTo(1234); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - monotonicClock.writeXml(out, Xml.newBinarySerializer()); + monotonicClock.write(); mClock.realtime = 42; - MonotonicClock newMonotonicClock = new MonotonicClock(0, mClock); - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - newMonotonicClock.readXml(in, Xml.newBinaryPullParser()); + MonotonicClock newMonotonicClock = new MonotonicClock(mFile, 0, mClock); mClock.realtime = 2000; assertThat(newMonotonicClock.monotonicTime()).isEqualTo(1234 - 42 + 2000); } + + @Test + public void constructor() { + MonotonicClock monotonicClock = new MonotonicClock(null, 1000, mClock); + mClock.realtime = 234; + + assertThat(monotonicClock.monotonicTime()).isEqualTo(1234); + } + + @Test + public void corruptedFile() throws IOException { + // Create an invalid binary XML file to cause IOException: "Unexpected magic number" + try (FileWriter w = new FileWriter(mFile)) { + w.write("garbage"); + } + + MonotonicClock monotonicClock = new MonotonicClock(mFile, 1000, mClock); + mClock.realtime = 234; + + assertThat(monotonicClock.monotonicTime()).isEqualTo(1234); + } } diff --git a/data/etc/com.android.documentsui.xml b/data/etc/com.android.documentsui.xml index d32cbecb16ec..2e521e30c673 100644 --- a/data/etc/com.android.documentsui.xml +++ b/data/etc/com.android.documentsui.xml @@ -23,5 +23,7 @@ <permission name="android.permission.MODIFY_QUIET_MODE"/> <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG"/> <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/> + <!-- Permissions required for reading device configs --> + <permission name="android.permission.READ_DEVICE_CONFIG" /> </privapp-permissions> </permissions> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index a1ea2b8ce032..f7c278c76838 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -426,6 +426,7 @@ applications that come with the platform <!-- Permissions required for CTS test - android.server.biometrics --> <permission name="android.permission.USE_BIOMETRIC" /> <permission name="android.permission.TEST_BIOMETRIC" /> + <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> <!-- Permissions required for CTS test - CtsContactsProviderTestCases --> <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" /> <!-- Permissions required for CTS test - CtsHdmiCecHostTestCases --> @@ -511,6 +512,7 @@ applications that come with the platform <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <!-- Permission required for CTS test - CtsTelephonyTestCases --> <permission name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" /> + <permission name="android.permission.ACCESS_LAST_KNOWN_CELL_ID" /> <!-- Permission required for CTS test - CtsAppTestCases --> <permission name="android.permission.CAPTURE_MEDIA_OUTPUT" /> <permission name="android.permission.CAPTURE_TUNER_AUDIO_INPUT" /> @@ -541,6 +543,8 @@ applications that come with the platform <permission name="android.permission.GET_BINDING_UID_IMPORTANCE"/> <!-- Permission required for CTS test NotificationManagerZenTest --> <permission name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" /> + <!-- Permission required for BinaryTransparencyService shell API and host test --> + <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 917a30061aca..3a778c314606 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -535,6 +535,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/ActivityStarter.java" }, + "-1583619037": { + "message": "Failed to register MediaProjectionWatcherCallback", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/ScreenRecordingCallbackController.java" + }, "-1582845629": { "message": "Starting animation on %s", "level": "VERBOSE", @@ -2983,12 +2989,6 @@ "group": "WM_DEBUG_SCREEN_ON", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "466506262": { - "message": "Clear freezing of %s: visible=%b freezing=%b", - "level": "VERBOSE", - "group": "WM_DEBUG_ORIENTATION", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "485170982": { "message": "Not finishing noHistory %s on stop because we're just sleeping", "level": "DEBUG", diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp index 3dd9ba9db1d9..471acaa0b1b6 100644 --- a/data/fonts/Android.bp +++ b/data/fonts/Android.bp @@ -48,12 +48,35 @@ prebuilt_font { // Copies the font configuration file into system/etc for the product as fonts.xml. // Additional fonts should be installed to /product/fonts/ alongside a corresponding // fonts_customiztion.xml in /product/etc/ -prebuilt_etc { + +soong_config_bool_variable { + name: "use_var_font", +} + +soong_config_module_type { + name: "prebuilt_fonts_xml", + module_type: "prebuilt_etc", + config_namespace: "noto_sans_cjk_config", + bool_variables: ["use_var_font"], + properties: ["src"], +} + +prebuilt_fonts_xml { name: "fonts.xml", src: "fonts.xml", + soong_config_variables: { + use_var_font: { + src: "fonts_cjkvf.xml", + }, + }, } -prebuilt_etc { +prebuilt_fonts_xml { name: "font_fallback.xml", src: "font_fallback.xml", + soong_config_variables: { + use_var_font: { + src: "font_fallback_cjkvf.xml", + }, + }, } diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml new file mode 100644 index 000000000000..70474bae11e9 --- /dev/null +++ b/data/fonts/font_fallback_cjkvf.xml @@ -0,0 +1,1015 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + In this file, all fonts without names are added to the default list. + Fonts are chosen based on a match: full BCP-47 language tag including + script, then just language, and finally order (the first font containing + the glyph). + + Order of appearance is also the tiebreaker for weight matching. This is + the reason why the 900 weights of Roboto precede the 700 weights - we + prefer the former when an 800 weight is requested. Since bold spans + effectively add 300 to the weight, this ensures that 900 is the bold + paired with the 500 weight, ensuring adequate contrast. + + TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required +--> +<familyset version="23"> + <!-- first font is default --> + <family name="sans-serif" varFamilyType="2"> + <font>Roboto-Regular.ttf + <axis tag="wdth" stylevalue="100" /> + </font> + </family> + + + <!-- Note that aliases must come after the fonts they reference. --> + <alias name="sans-serif-thin" to="sans-serif" weight="100" /> + <alias name="sans-serif-light" to="sans-serif" weight="300" /> + <alias name="sans-serif-medium" to="sans-serif" weight="500" /> + <alias name="sans-serif-black" to="sans-serif" weight="900" /> + <alias name="arial" to="sans-serif" /> + <alias name="helvetica" to="sans-serif" /> + <alias name="tahoma" to="sans-serif" /> + <alias name="verdana" to="sans-serif" /> + + <family name="sans-serif-condensed" varFamilyType="2"> + <font>Roboto-Regular.ttf + <axis tag="wdth" stylevalue="75" /> + </font> + </family> + <alias name="sans-serif-condensed-light" to="sans-serif-condensed" weight="300" /> + <alias name="sans-serif-condensed-medium" to="sans-serif-condensed" weight="500" /> + + <family name="serif"> + <font weight="400" style="normal" postScriptName="NotoSerif">NotoSerif-Regular.ttf</font> + <font weight="700" style="normal">NotoSerif-Bold.ttf</font> + <font weight="400" style="italic">NotoSerif-Italic.ttf</font> + <font weight="700" style="italic">NotoSerif-BoldItalic.ttf</font> + </family> + <alias name="serif-bold" to="serif" weight="700" /> + <alias name="times" to="serif" /> + <alias name="times new roman" to="serif" /> + <alias name="palatino" to="serif" /> + <alias name="georgia" to="serif" /> + <alias name="baskerville" to="serif" /> + <alias name="goudy" to="serif" /> + <alias name="fantasy" to="serif" /> + <alias name="ITC Stone Serif" to="serif" /> + + <family name="monospace"> + <font weight="400" style="normal">DroidSansMono.ttf</font> + </family> + <alias name="sans-serif-monospace" to="monospace" /> + <alias name="monaco" to="monospace" /> + + <family name="serif-monospace"> + <font weight="400" style="normal" postScriptName="CutiveMono-Regular">CutiveMono.ttf</font> + </family> + <alias name="courier" to="serif-monospace" /> + <alias name="courier new" to="serif-monospace" /> + + <family name="casual"> + <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font> + </family> + + <family name="cursive" varFamilyType="1"> + <font>DancingScript-Regular.ttf</font> + </family> + + <family name="sans-serif-smallcaps"> + <font weight="400" style="normal">CarroisGothicSC-Regular.ttf</font> + </family> + + <family name="source-sans-pro"> + <font weight="400" style="normal">SourceSansPro-Regular.ttf</font> + <font weight="400" style="italic">SourceSansPro-Italic.ttf</font> + <font weight="600" style="normal">SourceSansPro-SemiBold.ttf</font> + <font weight="600" style="italic">SourceSansPro-SemiBoldItalic.ttf</font> + <font weight="700" style="normal">SourceSansPro-Bold.ttf</font> + <font weight="700" style="italic">SourceSansPro-BoldItalic.ttf</font> + </family> + <alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/> + + <family name="roboto-flex" varFamilyType="2"> + <font>RobotoFlex-Regular.ttf + <axis tag="wdth" stylevalue="100" /> + </font> + </family> + + <!-- fallback fonts --> + <family lang="und-Arab" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabic"> + NotoNaskhArabic-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabic-Bold.ttf</font> + </family> + <family lang="und-Arab" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabicUI"> + NotoNaskhArabicUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font> + </family> + <family lang="und-Ethi" varFamilyType="1"> + <font postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular"> + NotoSerifEthiopic-VF.ttf + </font> + </family> + <family lang="und-Hebr"> + <font weight="400" style="normal" postScriptName="NotoSansHebrew"> + NotoSansHebrew-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansHebrew-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifHebrew-Regular.ttf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifHebrew-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansThai">NotoSansThai-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThai-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifThai-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifThai-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansThaiUI"> + NotoSansThaiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font> + </family> + <family lang="und-Armn" varFamilyType="1"> + <font postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular"> + NotoSerifArmenian-VF.ttf + </font> + </family> + <family lang="und-Geor,und-Geok" varFamilyType="1"> + <font postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular"> + NotoSerifGeorgian-VF.ttf + </font> + </family> + <family lang="und-Deva" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular"> + NotoSerifDevanagari-VF.ttf + </font> + </family> + <family lang="und-Deva" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + </font> + </family> + + <!-- All scripts of India should come after Devanagari, due to shared + danda characters. + --> + <family lang="und-Gujr" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansGujarati"> + NotoSansGujarati-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Gujr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansGujaratiUI"> + NotoSansGujaratiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font> + </family> + <family lang="und-Guru" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular"> + NotoSerifGurmukhi-VF.ttf + </font> + </family> + <family lang="und-Guru" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + </font> + </family> + <family lang="und-Taml" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular"> + NotoSerifTamil-VF.ttf + </font> + </family> + <family lang="und-Taml" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + </font> + </family> + <family lang="und-Mlym" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular"> + NotoSerifMalayalam-VF.ttf + </font> + </family> + <family lang="und-Mlym" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + </font> + </family> + <family lang="und-Beng" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular"> + NotoSerifBengali-VF.ttf + </font> + </family> + <family lang="und-Beng" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + </font> + </family> + <family lang="und-Telu" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular"> + NotoSerifTelugu-VF.ttf + </font> + </family> + <family lang="und-Telu" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + </font> + </family> + <family lang="und-Knda" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular"> + NotoSerifKannada-VF.ttf + </font> + </family> + <family lang="und-Knda" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + </font> + </family> + <family lang="und-Orya" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansOriya">NotoSansOriya-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriya-Bold.ttf</font> + </family> + <family lang="und-Orya" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansOriyaUI"> + NotoSansOriyaUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font> + </family> + <family lang="und-Sinh" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular"> + NotoSerifSinhala-VF.ttf + </font> + </family> + <family lang="und-Sinh" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + </font> + </family> + <family lang="und-Khmr" variant="elegant"> + <font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="26.0"/> + </font> + <font weight="200" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="39.0"/> + </font> + <font weight="300" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="58.0"/> + </font> + <font weight="400" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="90.0"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="108.0"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="128.0"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="151.0"/> + </font> + <font weight="800" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="169.0"/> + </font> + <font weight="900" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="190.0"/> + </font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifKhmer-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifKhmer-Bold.otf</font> + </family> + <family lang="und-Khmr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansKhmerUI"> + NotoSansKhmerUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansKhmerUI-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="elegant"> + <font weight="400" style="normal">NotoSansLao-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLao-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifLao-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifLao-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansLaoUI">NotoSansLaoUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLaoUI-Bold.ttf</font> + </family> + <family lang="und-Mymr" variant="elegant"> + <font weight="400" style="normal">NotoSansMyanmar-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmar-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmar-Bold.otf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifMyanmar-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifMyanmar-Bold.otf</font> + </family> + <family lang="und-Mymr" variant="compact"> + <font weight="400" style="normal">NotoSansMyanmarUI-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmarUI-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmarUI-Bold.otf</font> + </family> + <family lang="und-Thaa"> + <font weight="400" style="normal" postScriptName="NotoSansThaana"> + NotoSansThaana-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaana-Bold.ttf</font> + </family> + <family lang="und-Cham"> + <font weight="400" style="normal" postScriptName="NotoSansCham">NotoSansCham-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansCham-Bold.ttf</font> + </family> + <family lang="und-Ahom"> + <font weight="400" style="normal">NotoSansAhom-Regular.otf</font> + </family> + <family lang="und-Adlm" varFamilyType="1"> + <font postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + </font> + </family> + <family lang="und-Avst"> + <font weight="400" style="normal" postScriptName="NotoSansAvestan"> + NotoSansAvestan-Regular.ttf + </font> + </family> + <family lang="und-Bali"> + <font weight="400" style="normal" postScriptName="NotoSansBalinese"> + NotoSansBalinese-Regular.ttf + </font> + </family> + <family lang="und-Bamu"> + <font weight="400" style="normal" postScriptName="NotoSansBamum">NotoSansBamum-Regular.ttf + </font> + </family> + <family lang="und-Batk"> + <font weight="400" style="normal" postScriptName="NotoSansBatak">NotoSansBatak-Regular.ttf + </font> + </family> + <family lang="und-Brah"> + <font weight="400" style="normal" postScriptName="NotoSansBrahmi"> + NotoSansBrahmi-Regular.ttf + </font> + </family> + <family lang="und-Bugi"> + <font weight="400" style="normal" postScriptName="NotoSansBuginese"> + NotoSansBuginese-Regular.ttf + </font> + </family> + <family lang="und-Buhd"> + <font weight="400" style="normal" postScriptName="NotoSansBuhid">NotoSansBuhid-Regular.ttf + </font> + </family> + <family lang="und-Cans"> + <font weight="400" style="normal"> + NotoSansCanadianAboriginal-Regular.ttf + </font> + </family> + <family lang="und-Cari"> + <font weight="400" style="normal" postScriptName="NotoSansCarian"> + NotoSansCarian-Regular.ttf + </font> + </family> + <family lang="und-Cakm"> + <font weight="400" style="normal">NotoSansChakma-Regular.otf</font> + </family> + <family lang="und-Cher"> + <font weight="400" style="normal">NotoSansCherokee-Regular.ttf</font> + </family> + <family lang="und-Copt"> + <font weight="400" style="normal" postScriptName="NotoSansCoptic"> + NotoSansCoptic-Regular.ttf + </font> + </family> + <family lang="und-Xsux"> + <font weight="400" style="normal" postScriptName="NotoSansCuneiform"> + NotoSansCuneiform-Regular.ttf + </font> + </family> + <family lang="und-Cprt"> + <font weight="400" style="normal" postScriptName="NotoSansCypriot"> + NotoSansCypriot-Regular.ttf + </font> + </family> + <family lang="und-Dsrt"> + <font weight="400" style="normal" postScriptName="NotoSansDeseret"> + NotoSansDeseret-Regular.ttf + </font> + </family> + <family lang="und-Egyp"> + <font weight="400" style="normal" postScriptName="NotoSansEgyptianHieroglyphs"> + NotoSansEgyptianHieroglyphs-Regular.ttf + </font> + </family> + <family lang="und-Elba"> + <font weight="400" style="normal">NotoSansElbasan-Regular.otf</font> + </family> + <family lang="und-Glag"> + <font weight="400" style="normal" postScriptName="NotoSansGlagolitic"> + NotoSansGlagolitic-Regular.ttf + </font> + </family> + <family lang="und-Goth"> + <font weight="400" style="normal" postScriptName="NotoSansGothic"> + NotoSansGothic-Regular.ttf + </font> + </family> + <family lang="und-Hano"> + <font weight="400" style="normal" postScriptName="NotoSansHanunoo"> + NotoSansHanunoo-Regular.ttf + </font> + </family> + <family lang="und-Armi"> + <font weight="400" style="normal" postScriptName="NotoSansImperialAramaic"> + NotoSansImperialAramaic-Regular.ttf + </font> + </family> + <family lang="und-Phli"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalPahlavi"> + NotoSansInscriptionalPahlavi-Regular.ttf + </font> + </family> + <family lang="und-Prti"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalParthian"> + NotoSansInscriptionalParthian-Regular.ttf + </font> + </family> + <family lang="und-Java"> + <font weight="400" style="normal">NotoSansJavanese-Regular.otf</font> + </family> + <family lang="und-Kthi"> + <font weight="400" style="normal" postScriptName="NotoSansKaithi"> + NotoSansKaithi-Regular.ttf + </font> + </family> + <family lang="und-Kali"> + <font weight="400" style="normal" postScriptName="NotoSansKayahLi"> + NotoSansKayahLi-Regular.ttf + </font> + </family> + <family lang="und-Khar"> + <font weight="400" style="normal" postScriptName="NotoSansKharoshthi"> + NotoSansKharoshthi-Regular.ttf + </font> + </family> + <family lang="und-Lepc"> + <font weight="400" style="normal" postScriptName="NotoSansLepcha"> + NotoSansLepcha-Regular.ttf + </font> + </family> + <family lang="und-Limb"> + <font weight="400" style="normal" postScriptName="NotoSansLimbu">NotoSansLimbu-Regular.ttf + </font> + </family> + <family lang="und-Linb"> + <font weight="400" style="normal" postScriptName="NotoSansLinearB"> + NotoSansLinearB-Regular.ttf + </font> + </family> + <family lang="und-Lisu"> + <font weight="400" style="normal" postScriptName="NotoSansLisu">NotoSansLisu-Regular.ttf + </font> + </family> + <family lang="und-Lyci"> + <font weight="400" style="normal" postScriptName="NotoSansLycian"> + NotoSansLycian-Regular.ttf + </font> + </family> + <family lang="und-Lydi"> + <font weight="400" style="normal" postScriptName="NotoSansLydian"> + NotoSansLydian-Regular.ttf + </font> + </family> + <family lang="und-Mand"> + <font weight="400" style="normal" postScriptName="NotoSansMandaic"> + NotoSansMandaic-Regular.ttf + </font> + </family> + <family lang="und-Mtei"> + <font weight="400" style="normal" postScriptName="NotoSansMeeteiMayek"> + NotoSansMeeteiMayek-Regular.ttf + </font> + </family> + <family lang="und-Talu"> + <font weight="400" style="normal" postScriptName="NotoSansNewTaiLue"> + NotoSansNewTaiLue-Regular.ttf + </font> + </family> + <family lang="und-Nkoo"> + <font weight="400" style="normal" postScriptName="NotoSansNKo">NotoSansNKo-Regular.ttf + </font> + </family> + <family lang="und-Ogam"> + <font weight="400" style="normal" postScriptName="NotoSansOgham">NotoSansOgham-Regular.ttf + </font> + </family> + <family lang="und-Olck"> + <font weight="400" style="normal" postScriptName="NotoSansOlChiki"> + NotoSansOlChiki-Regular.ttf + </font> + </family> + <family lang="und-Ital"> + <font weight="400" style="normal" postScriptName="NotoSansOldItalic"> + NotoSansOldItalic-Regular.ttf + </font> + </family> + <family lang="und-Xpeo"> + <font weight="400" style="normal" postScriptName="NotoSansOldPersian"> + NotoSansOldPersian-Regular.ttf + </font> + </family> + <family lang="und-Sarb"> + <font weight="400" style="normal" postScriptName="NotoSansOldSouthArabian"> + NotoSansOldSouthArabian-Regular.ttf + </font> + </family> + <family lang="und-Orkh"> + <font weight="400" style="normal" postScriptName="NotoSansOldTurkic"> + NotoSansOldTurkic-Regular.ttf + </font> + </family> + <family lang="und-Osge"> + <font weight="400" style="normal">NotoSansOsage-Regular.ttf</font> + </family> + <family lang="und-Osma"> + <font weight="400" style="normal" postScriptName="NotoSansOsmanya"> + NotoSansOsmanya-Regular.ttf + </font> + </family> + <family lang="und-Phnx"> + <font weight="400" style="normal" postScriptName="NotoSansPhoenician"> + NotoSansPhoenician-Regular.ttf + </font> + </family> + <family lang="und-Rjng"> + <font weight="400" style="normal" postScriptName="NotoSansRejang"> + NotoSansRejang-Regular.ttf + </font> + </family> + <family lang="und-Runr"> + <font weight="400" style="normal" postScriptName="NotoSansRunic">NotoSansRunic-Regular.ttf + </font> + </family> + <family lang="und-Samr"> + <font weight="400" style="normal" postScriptName="NotoSansSamaritan"> + NotoSansSamaritan-Regular.ttf + </font> + </family> + <family lang="und-Saur"> + <font weight="400" style="normal" postScriptName="NotoSansSaurashtra"> + NotoSansSaurashtra-Regular.ttf + </font> + </family> + <family lang="und-Shaw"> + <font weight="400" style="normal" postScriptName="NotoSansShavian"> + NotoSansShavian-Regular.ttf + </font> + </family> + <family lang="und-Sund"> + <font weight="400" style="normal" postScriptName="NotoSansSundanese"> + NotoSansSundanese-Regular.ttf + </font> + </family> + <family lang="und-Sylo"> + <font weight="400" style="normal" postScriptName="NotoSansSylotiNagri"> + NotoSansSylotiNagri-Regular.ttf + </font> + </family> + <!-- Esrangela should precede Eastern and Western Syriac, since it's our default form. --> + <family lang="und-Syre"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEstrangela"> + NotoSansSyriacEstrangela-Regular.ttf + </font> + </family> + <family lang="und-Syrn"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEastern"> + NotoSansSyriacEastern-Regular.ttf + </font> + </family> + <family lang="und-Syrj"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacWestern"> + NotoSansSyriacWestern-Regular.ttf + </font> + </family> + <family lang="und-Tglg"> + <font weight="400" style="normal" postScriptName="NotoSansTagalog"> + NotoSansTagalog-Regular.ttf + </font> + </family> + <family lang="und-Tagb"> + <font weight="400" style="normal" postScriptName="NotoSansTagbanwa"> + NotoSansTagbanwa-Regular.ttf + </font> + </family> + <family lang="und-Lana"> + <font weight="400" style="normal" postScriptName="NotoSansTaiTham"> + NotoSansTaiTham-Regular.ttf + </font> + </family> + <family lang="und-Tavt"> + <font weight="400" style="normal" postScriptName="NotoSansTaiViet"> + NotoSansTaiViet-Regular.ttf + </font> + </family> + <family lang="und-Tibt" varFamilyType="1"> + <font postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + </font> + </family> + <family lang="und-Tfng"> + <font weight="400" style="normal">NotoSansTifinagh-Regular.otf</font> + </family> + <family lang="und-Ugar"> + <font weight="400" style="normal" postScriptName="NotoSansUgaritic"> + NotoSansUgaritic-Regular.ttf + </font> + </family> + <family lang="und-Vaii"> + <font weight="400" style="normal" postScriptName="NotoSansVai">NotoSansVai-Regular.ttf + </font> + </family> + <family> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted.ttf</font> + </family> + <family lang="zh-Hans"> + <font weight="100" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="2" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="zh-Hant,zh-Bopo"> + <font weight="100" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="3" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ja"> + <font weight="100" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="0" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ko"> + <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="1" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmoji.ttf</font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmojiFlags.ttf</font> + </family> + <family lang="und-Zsym"> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted2.ttf</font> + </family> + <!-- + Tai Le, Yi, Mongolian, and Phags-pa are intentionally kept last, to make sure they don't + override the East Asian punctuation for Chinese. + --> + <family lang="und-Tale"> + <font weight="400" style="normal" postScriptName="NotoSansTaiLe">NotoSansTaiLe-Regular.ttf + </font> + </family> + <family lang="und-Yiii"> + <font weight="400" style="normal" postScriptName="NotoSansYi">NotoSansYi-Regular.ttf</font> + </family> + <family lang="und-Mong"> + <font weight="400" style="normal" postScriptName="NotoSansMongolian"> + NotoSansMongolian-Regular.ttf + </font> + </family> + <family lang="und-Phag"> + <font weight="400" style="normal" postScriptName="NotoSansPhagsPa"> + NotoSansPhagsPa-Regular.ttf + </font> + </family> + <family lang="und-Hluw"> + <font weight="400" style="normal">NotoSansAnatolianHieroglyphs-Regular.otf</font> + </family> + <family lang="und-Bass"> + <font weight="400" style="normal">NotoSansBassaVah-Regular.otf</font> + </family> + <family lang="und-Bhks"> + <font weight="400" style="normal">NotoSansBhaiksuki-Regular.otf</font> + </family> + <family lang="und-Hatr"> + <font weight="400" style="normal">NotoSansHatran-Regular.otf</font> + </family> + <family lang="und-Lina"> + <font weight="400" style="normal">NotoSansLinearA-Regular.otf</font> + </family> + <family lang="und-Mani"> + <font weight="400" style="normal">NotoSansManichaean-Regular.otf</font> + </family> + <family lang="und-Marc"> + <font weight="400" style="normal">NotoSansMarchen-Regular.otf</font> + </family> + <family lang="und-Merc"> + <font weight="400" style="normal">NotoSansMeroitic-Regular.otf</font> + </family> + <family lang="und-Plrd"> + <font weight="400" style="normal">NotoSansMiao-Regular.otf</font> + </family> + <family lang="und-Mroo"> + <font weight="400" style="normal">NotoSansMro-Regular.otf</font> + </family> + <family lang="und-Mult"> + <font weight="400" style="normal">NotoSansMultani-Regular.otf</font> + </family> + <family lang="und-Nbat"> + <font weight="400" style="normal">NotoSansNabataean-Regular.otf</font> + </family> + <family lang="und-Newa"> + <font weight="400" style="normal">NotoSansNewa-Regular.otf</font> + </family> + <family lang="und-Narb"> + <font weight="400" style="normal">NotoSansOldNorthArabian-Regular.otf</font> + </family> + <family lang="und-Perm"> + <font weight="400" style="normal">NotoSansOldPermic-Regular.otf</font> + </family> + <family lang="und-Hmng"> + <font weight="400" style="normal">NotoSansPahawhHmong-Regular.otf</font> + </family> + <family lang="und-Palm"> + <font weight="400" style="normal">NotoSansPalmyrene-Regular.otf</font> + </family> + <family lang="und-Pauc"> + <font weight="400" style="normal">NotoSansPauCinHau-Regular.otf</font> + </family> + <family lang="und-Shrd"> + <font weight="400" style="normal">NotoSansSharada-Regular.otf</font> + </family> + <family lang="und-Sora"> + <font weight="400" style="normal">NotoSansSoraSompeng-Regular.otf</font> + </family> + <family lang="und-Gong"> + <font weight="400" style="normal">NotoSansGunjalaGondi-Regular.otf</font> + </family> + <family lang="und-Rohg"> + <font weight="400" style="normal">NotoSansHanifiRohingya-Regular.otf</font> + </family> + <family lang="und-Khoj"> + <font weight="400" style="normal">NotoSansKhojki-Regular.otf</font> + </family> + <family lang="und-Gonm"> + <font weight="400" style="normal">NotoSansMasaramGondi-Regular.otf</font> + </family> + <family lang="und-Wcho"> + <font weight="400" style="normal">NotoSansWancho-Regular.otf</font> + </family> + <family lang="und-Wara"> + <font weight="400" style="normal">NotoSansWarangCiti-Regular.otf</font> + </family> + <family lang="und-Gran"> + <font weight="400" style="normal">NotoSansGrantha-Regular.ttf</font> + </family> + <family lang="und-Modi"> + <font weight="400" style="normal">NotoSansModi-Regular.ttf</font> + </family> + <family lang="und-Dogr"> + <font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font> + </family> + <family lang="und-Medf" varFamilyType="1"> + <font postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + </font> + </family> + <family lang="und-Soyo" varFamilyType="1"> + <font postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + </font> + </family> + <family lang="und-Takr" varFamilyType="1"> + <font postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + </font> + </family> + <family lang="und-Hmnp" varFamilyType="1"> + <font postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + </font> + </family> + <family lang="und-Yezi" varFamilyType="1"> + <font postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + </font> + </family> +</familyset> diff --git a/data/fonts/fonts_cjkvf.xml b/data/fonts/fonts_cjkvf.xml new file mode 100644 index 000000000000..8d91eddd807d --- /dev/null +++ b/data/fonts/fonts_cjkvf.xml @@ -0,0 +1,1785 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + DEPRECATED: This XML file is no longer a source of the font files installed + in the system. + + For the device vendors: please add your font configurations to the + platform/frameworks/base/data/font_fallback.xml and also add it to this XML + file as much as possible for apps that reads this XML file. + + For the application developers: please stop reading this XML file and use + android.graphics.fonts.SystemFonts#getAvailableFonts Java API or + ASystemFontIterator_open NDK API for getting list of system installed + font files. + + WARNING: Parsing of this file by third-party apps is not supported. The + file, and the font files it refers to, will be renamed and/or moved out + from their respective location in the next Android release, and/or the + format or syntax of the file may change significantly. If you parse this + file for information about system fonts, do it at your own risk. Your + application will almost certainly break with the next major Android + release. + + In this file, all fonts without names are added to the default list. + Fonts are chosen based on a match: full BCP-47 language tag including + script, then just language, and finally order (the first font containing + the glyph). + + Order of appearance is also the tiebreaker for weight matching. This is + the reason why the 900 weights of Roboto precede the 700 weights - we + prefer the former when an 800 weight is requested. Since bold spans + effectively add 300 to the weight, this ensures that 900 is the bold + paired with the 500 weight, ensuring adequate contrast. + + TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required +--> +<familyset version="23"> + <!-- first font is default --> + <family name="sans-serif"> + <font weight="100" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + <font weight="100" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + </family> + + + <!-- Note that aliases must come after the fonts they reference. --> + <alias name="sans-serif-thin" to="sans-serif" weight="100" /> + <alias name="sans-serif-light" to="sans-serif" weight="300" /> + <alias name="sans-serif-medium" to="sans-serif" weight="500" /> + <alias name="sans-serif-black" to="sans-serif" weight="900" /> + <alias name="arial" to="sans-serif" /> + <alias name="helvetica" to="sans-serif" /> + <alias name="tahoma" to="sans-serif" /> + <alias name="verdana" to="sans-serif" /> + + <family name="sans-serif-condensed"> + <font weight="100" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="900" /> + </font> + <font weight="100" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="900" /> + </font> + </family> + <alias name="sans-serif-condensed-light" to="sans-serif-condensed" weight="300" /> + <alias name="sans-serif-condensed-medium" to="sans-serif-condensed" weight="500" /> + + <family name="serif"> + <font weight="400" style="normal" postScriptName="NotoSerif">NotoSerif-Regular.ttf</font> + <font weight="700" style="normal">NotoSerif-Bold.ttf</font> + <font weight="400" style="italic">NotoSerif-Italic.ttf</font> + <font weight="700" style="italic">NotoSerif-BoldItalic.ttf</font> + </family> + <alias name="serif-bold" to="serif" weight="700" /> + <alias name="times" to="serif" /> + <alias name="times new roman" to="serif" /> + <alias name="palatino" to="serif" /> + <alias name="georgia" to="serif" /> + <alias name="baskerville" to="serif" /> + <alias name="goudy" to="serif" /> + <alias name="fantasy" to="serif" /> + <alias name="ITC Stone Serif" to="serif" /> + + <family name="monospace"> + <font weight="400" style="normal">DroidSansMono.ttf</font> + </family> + <alias name="sans-serif-monospace" to="monospace" /> + <alias name="monaco" to="monospace" /> + + <family name="serif-monospace"> + <font weight="400" style="normal" postScriptName="CutiveMono-Regular">CutiveMono.ttf</font> + </family> + <alias name="courier" to="serif-monospace" /> + <alias name="courier new" to="serif-monospace" /> + + <family name="casual"> + <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font> + </family> + + <family name="cursive"> + <font weight="400" style="normal">DancingScript-Regular.ttf + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="700" style="normal">DancingScript-Regular.ttf + <axis tag="wght" stylevalue="700" /> + </font> + </family> + + <family name="sans-serif-smallcaps"> + <font weight="400" style="normal">CarroisGothicSC-Regular.ttf</font> + </family> + + <family name="source-sans-pro"> + <font weight="400" style="normal">SourceSansPro-Regular.ttf</font> + <font weight="400" style="italic">SourceSansPro-Italic.ttf</font> + <font weight="600" style="normal">SourceSansPro-SemiBold.ttf</font> + <font weight="600" style="italic">SourceSansPro-SemiBoldItalic.ttf</font> + <font weight="700" style="normal">SourceSansPro-Bold.ttf</font> + <font weight="700" style="italic">SourceSansPro-BoldItalic.ttf</font> + </family> + <alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/> + + <family name="roboto-flex"> + <font weight="100" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + <font weight="100" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + </family> + + <!-- fallback fonts --> + <family lang="und-Arab" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabic"> + NotoNaskhArabic-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabic-Bold.ttf</font> + </family> + <family lang="und-Arab" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabicUI"> + NotoNaskhArabicUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font> + </family> + <family lang="und-Ethi"> + <font weight="400" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Hebr"> + <font weight="400" style="normal" postScriptName="NotoSansHebrew"> + NotoSansHebrew-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansHebrew-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifHebrew-Regular.ttf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifHebrew-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansThai">NotoSansThai-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThai-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifThai-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifThai-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansThaiUI"> + NotoSansThaiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font> + </family> + <family lang="und-Armn"> + <font weight="400" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Geor,und-Geok"> + <font weight="400" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Deva" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Deva" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + + <!-- All scripts of India should come after Devanagari, due to shared + danda characters. + --> + <family lang="und-Gujr" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansGujarati"> + NotoSansGujarati-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Gujr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansGujaratiUI"> + NotoSansGujaratiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font> + </family> + <family lang="und-Guru" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Guru" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Taml" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Taml" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Mlym" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Mlym" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Beng" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Beng" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Telu" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Telu" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Knda" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Knda" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Orya" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansOriya">NotoSansOriya-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriya-Bold.ttf</font> + </family> + <family lang="und-Orya" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansOriyaUI"> + NotoSansOriyaUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font> + </family> + <family lang="und-Sinh" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Sinh" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Khmr" variant="elegant"> + <font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="26.0"/> + </font> + <font weight="200" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="39.0"/> + </font> + <font weight="300" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="58.0"/> + </font> + <font weight="400" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="90.0"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="108.0"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="128.0"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="151.0"/> + </font> + <font weight="800" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="169.0"/> + </font> + <font weight="900" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="190.0"/> + </font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifKhmer-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifKhmer-Bold.otf</font> + </family> + <family lang="und-Khmr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansKhmerUI"> + NotoSansKhmerUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansKhmerUI-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="elegant"> + <font weight="400" style="normal">NotoSansLao-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLao-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifLao-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifLao-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansLaoUI">NotoSansLaoUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLaoUI-Bold.ttf</font> + </family> + <family lang="und-Mymr" variant="elegant"> + <font weight="400" style="normal">NotoSansMyanmar-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmar-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmar-Bold.otf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifMyanmar-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifMyanmar-Bold.otf</font> + </family> + <family lang="und-Mymr" variant="compact"> + <font weight="400" style="normal">NotoSansMyanmarUI-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmarUI-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmarUI-Bold.otf</font> + </family> + <family lang="und-Thaa"> + <font weight="400" style="normal" postScriptName="NotoSansThaana"> + NotoSansThaana-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaana-Bold.ttf</font> + </family> + <family lang="und-Cham"> + <font weight="400" style="normal" postScriptName="NotoSansCham">NotoSansCham-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansCham-Bold.ttf</font> + </family> + <family lang="und-Ahom"> + <font weight="400" style="normal">NotoSansAhom-Regular.otf</font> + </family> + <family lang="und-Adlm"> + <font weight="400" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Avst"> + <font weight="400" style="normal" postScriptName="NotoSansAvestan"> + NotoSansAvestan-Regular.ttf + </font> + </family> + <family lang="und-Bali"> + <font weight="400" style="normal" postScriptName="NotoSansBalinese"> + NotoSansBalinese-Regular.ttf + </font> + </family> + <family lang="und-Bamu"> + <font weight="400" style="normal" postScriptName="NotoSansBamum">NotoSansBamum-Regular.ttf + </font> + </family> + <family lang="und-Batk"> + <font weight="400" style="normal" postScriptName="NotoSansBatak">NotoSansBatak-Regular.ttf + </font> + </family> + <family lang="und-Brah"> + <font weight="400" style="normal" postScriptName="NotoSansBrahmi"> + NotoSansBrahmi-Regular.ttf + </font> + </family> + <family lang="und-Bugi"> + <font weight="400" style="normal" postScriptName="NotoSansBuginese"> + NotoSansBuginese-Regular.ttf + </font> + </family> + <family lang="und-Buhd"> + <font weight="400" style="normal" postScriptName="NotoSansBuhid">NotoSansBuhid-Regular.ttf + </font> + </family> + <family lang="und-Cans"> + <font weight="400" style="normal"> + NotoSansCanadianAboriginal-Regular.ttf + </font> + </family> + <family lang="und-Cari"> + <font weight="400" style="normal" postScriptName="NotoSansCarian"> + NotoSansCarian-Regular.ttf + </font> + </family> + <family lang="und-Cakm"> + <font weight="400" style="normal">NotoSansChakma-Regular.otf</font> + </family> + <family lang="und-Cher"> + <font weight="400" style="normal">NotoSansCherokee-Regular.ttf</font> + </family> + <family lang="und-Copt"> + <font weight="400" style="normal" postScriptName="NotoSansCoptic"> + NotoSansCoptic-Regular.ttf + </font> + </family> + <family lang="und-Xsux"> + <font weight="400" style="normal" postScriptName="NotoSansCuneiform"> + NotoSansCuneiform-Regular.ttf + </font> + </family> + <family lang="und-Cprt"> + <font weight="400" style="normal" postScriptName="NotoSansCypriot"> + NotoSansCypriot-Regular.ttf + </font> + </family> + <family lang="und-Dsrt"> + <font weight="400" style="normal" postScriptName="NotoSansDeseret"> + NotoSansDeseret-Regular.ttf + </font> + </family> + <family lang="und-Egyp"> + <font weight="400" style="normal" postScriptName="NotoSansEgyptianHieroglyphs"> + NotoSansEgyptianHieroglyphs-Regular.ttf + </font> + </family> + <family lang="und-Elba"> + <font weight="400" style="normal">NotoSansElbasan-Regular.otf</font> + </family> + <family lang="und-Glag"> + <font weight="400" style="normal" postScriptName="NotoSansGlagolitic"> + NotoSansGlagolitic-Regular.ttf + </font> + </family> + <family lang="und-Goth"> + <font weight="400" style="normal" postScriptName="NotoSansGothic"> + NotoSansGothic-Regular.ttf + </font> + </family> + <family lang="und-Hano"> + <font weight="400" style="normal" postScriptName="NotoSansHanunoo"> + NotoSansHanunoo-Regular.ttf + </font> + </family> + <family lang="und-Armi"> + <font weight="400" style="normal" postScriptName="NotoSansImperialAramaic"> + NotoSansImperialAramaic-Regular.ttf + </font> + </family> + <family lang="und-Phli"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalPahlavi"> + NotoSansInscriptionalPahlavi-Regular.ttf + </font> + </family> + <family lang="und-Prti"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalParthian"> + NotoSansInscriptionalParthian-Regular.ttf + </font> + </family> + <family lang="und-Java"> + <font weight="400" style="normal">NotoSansJavanese-Regular.otf</font> + </family> + <family lang="und-Kthi"> + <font weight="400" style="normal" postScriptName="NotoSansKaithi"> + NotoSansKaithi-Regular.ttf + </font> + </family> + <family lang="und-Kali"> + <font weight="400" style="normal" postScriptName="NotoSansKayahLi"> + NotoSansKayahLi-Regular.ttf + </font> + </family> + <family lang="und-Khar"> + <font weight="400" style="normal" postScriptName="NotoSansKharoshthi"> + NotoSansKharoshthi-Regular.ttf + </font> + </family> + <family lang="und-Lepc"> + <font weight="400" style="normal" postScriptName="NotoSansLepcha"> + NotoSansLepcha-Regular.ttf + </font> + </family> + <family lang="und-Limb"> + <font weight="400" style="normal" postScriptName="NotoSansLimbu">NotoSansLimbu-Regular.ttf + </font> + </family> + <family lang="und-Linb"> + <font weight="400" style="normal" postScriptName="NotoSansLinearB"> + NotoSansLinearB-Regular.ttf + </font> + </family> + <family lang="und-Lisu"> + <font weight="400" style="normal" postScriptName="NotoSansLisu">NotoSansLisu-Regular.ttf + </font> + </family> + <family lang="und-Lyci"> + <font weight="400" style="normal" postScriptName="NotoSansLycian"> + NotoSansLycian-Regular.ttf + </font> + </family> + <family lang="und-Lydi"> + <font weight="400" style="normal" postScriptName="NotoSansLydian"> + NotoSansLydian-Regular.ttf + </font> + </family> + <family lang="und-Mand"> + <font weight="400" style="normal" postScriptName="NotoSansMandaic"> + NotoSansMandaic-Regular.ttf + </font> + </family> + <family lang="und-Mtei"> + <font weight="400" style="normal" postScriptName="NotoSansMeeteiMayek"> + NotoSansMeeteiMayek-Regular.ttf + </font> + </family> + <family lang="und-Talu"> + <font weight="400" style="normal" postScriptName="NotoSansNewTaiLue"> + NotoSansNewTaiLue-Regular.ttf + </font> + </family> + <family lang="und-Nkoo"> + <font weight="400" style="normal" postScriptName="NotoSansNKo">NotoSansNKo-Regular.ttf + </font> + </family> + <family lang="und-Ogam"> + <font weight="400" style="normal" postScriptName="NotoSansOgham">NotoSansOgham-Regular.ttf + </font> + </family> + <family lang="und-Olck"> + <font weight="400" style="normal" postScriptName="NotoSansOlChiki"> + NotoSansOlChiki-Regular.ttf + </font> + </family> + <family lang="und-Ital"> + <font weight="400" style="normal" postScriptName="NotoSansOldItalic"> + NotoSansOldItalic-Regular.ttf + </font> + </family> + <family lang="und-Xpeo"> + <font weight="400" style="normal" postScriptName="NotoSansOldPersian"> + NotoSansOldPersian-Regular.ttf + </font> + </family> + <family lang="und-Sarb"> + <font weight="400" style="normal" postScriptName="NotoSansOldSouthArabian"> + NotoSansOldSouthArabian-Regular.ttf + </font> + </family> + <family lang="und-Orkh"> + <font weight="400" style="normal" postScriptName="NotoSansOldTurkic"> + NotoSansOldTurkic-Regular.ttf + </font> + </family> + <family lang="und-Osge"> + <font weight="400" style="normal">NotoSansOsage-Regular.ttf</font> + </family> + <family lang="und-Osma"> + <font weight="400" style="normal" postScriptName="NotoSansOsmanya"> + NotoSansOsmanya-Regular.ttf + </font> + </family> + <family lang="und-Phnx"> + <font weight="400" style="normal" postScriptName="NotoSansPhoenician"> + NotoSansPhoenician-Regular.ttf + </font> + </family> + <family lang="und-Rjng"> + <font weight="400" style="normal" postScriptName="NotoSansRejang"> + NotoSansRejang-Regular.ttf + </font> + </family> + <family lang="und-Runr"> + <font weight="400" style="normal" postScriptName="NotoSansRunic">NotoSansRunic-Regular.ttf + </font> + </family> + <family lang="und-Samr"> + <font weight="400" style="normal" postScriptName="NotoSansSamaritan"> + NotoSansSamaritan-Regular.ttf + </font> + </family> + <family lang="und-Saur"> + <font weight="400" style="normal" postScriptName="NotoSansSaurashtra"> + NotoSansSaurashtra-Regular.ttf + </font> + </family> + <family lang="und-Shaw"> + <font weight="400" style="normal" postScriptName="NotoSansShavian"> + NotoSansShavian-Regular.ttf + </font> + </family> + <family lang="und-Sund"> + <font weight="400" style="normal" postScriptName="NotoSansSundanese"> + NotoSansSundanese-Regular.ttf + </font> + </family> + <family lang="und-Sylo"> + <font weight="400" style="normal" postScriptName="NotoSansSylotiNagri"> + NotoSansSylotiNagri-Regular.ttf + </font> + </family> + <!-- Esrangela should precede Eastern and Western Syriac, since it's our default form. --> + <family lang="und-Syre"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEstrangela"> + NotoSansSyriacEstrangela-Regular.ttf + </font> + </family> + <family lang="und-Syrn"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEastern"> + NotoSansSyriacEastern-Regular.ttf + </font> + </family> + <family lang="und-Syrj"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacWestern"> + NotoSansSyriacWestern-Regular.ttf + </font> + </family> + <family lang="und-Tglg"> + <font weight="400" style="normal" postScriptName="NotoSansTagalog"> + NotoSansTagalog-Regular.ttf + </font> + </family> + <family lang="und-Tagb"> + <font weight="400" style="normal" postScriptName="NotoSansTagbanwa"> + NotoSansTagbanwa-Regular.ttf + </font> + </family> + <family lang="und-Lana"> + <font weight="400" style="normal" postScriptName="NotoSansTaiTham"> + NotoSansTaiTham-Regular.ttf + </font> + </family> + <family lang="und-Tavt"> + <font weight="400" style="normal" postScriptName="NotoSansTaiViet"> + NotoSansTaiViet-Regular.ttf + </font> + </family> + <family lang="und-Tibt"> + <font weight="400" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Tfng"> + <font weight="400" style="normal">NotoSansTifinagh-Regular.otf</font> + </family> + <family lang="und-Ugar"> + <font weight="400" style="normal" postScriptName="NotoSansUgaritic"> + NotoSansUgaritic-Regular.ttf + </font> + </family> + <family lang="und-Vaii"> + <font weight="400" style="normal" postScriptName="NotoSansVai">NotoSansVai-Regular.ttf + </font> + </family> + <family> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted.ttf</font> + </family> + <family lang="zh-Hans"> + <font weight="100" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="2" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="zh-Hant,zh-Bopo"> + <font weight="100" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="3" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ja"> + <font weight="100" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="0" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ko"> + <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="1" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="und-Zsye" ignore="true"> + <font weight="400" style="normal">NotoColorEmojiLegacy.ttf</font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmoji.ttf</font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmojiFlags.ttf</font> + </family> + <family lang="und-Zsym"> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted2.ttf</font> + </family> + <!-- + Tai Le, Yi, Mongolian, and Phags-pa are intentionally kept last, to make sure they don't + override the East Asian punctuation for Chinese. + --> + <family lang="und-Tale"> + <font weight="400" style="normal" postScriptName="NotoSansTaiLe">NotoSansTaiLe-Regular.ttf + </font> + </family> + <family lang="und-Yiii"> + <font weight="400" style="normal" postScriptName="NotoSansYi">NotoSansYi-Regular.ttf</font> + </family> + <family lang="und-Mong"> + <font weight="400" style="normal" postScriptName="NotoSansMongolian"> + NotoSansMongolian-Regular.ttf + </font> + </family> + <family lang="und-Phag"> + <font weight="400" style="normal" postScriptName="NotoSansPhagsPa"> + NotoSansPhagsPa-Regular.ttf + </font> + </family> + <family lang="und-Hluw"> + <font weight="400" style="normal">NotoSansAnatolianHieroglyphs-Regular.otf</font> + </family> + <family lang="und-Bass"> + <font weight="400" style="normal">NotoSansBassaVah-Regular.otf</font> + </family> + <family lang="und-Bhks"> + <font weight="400" style="normal">NotoSansBhaiksuki-Regular.otf</font> + </family> + <family lang="und-Hatr"> + <font weight="400" style="normal">NotoSansHatran-Regular.otf</font> + </family> + <family lang="und-Lina"> + <font weight="400" style="normal">NotoSansLinearA-Regular.otf</font> + </family> + <family lang="und-Mani"> + <font weight="400" style="normal">NotoSansManichaean-Regular.otf</font> + </family> + <family lang="und-Marc"> + <font weight="400" style="normal">NotoSansMarchen-Regular.otf</font> + </family> + <family lang="und-Merc"> + <font weight="400" style="normal">NotoSansMeroitic-Regular.otf</font> + </family> + <family lang="und-Plrd"> + <font weight="400" style="normal">NotoSansMiao-Regular.otf</font> + </family> + <family lang="und-Mroo"> + <font weight="400" style="normal">NotoSansMro-Regular.otf</font> + </family> + <family lang="und-Mult"> + <font weight="400" style="normal">NotoSansMultani-Regular.otf</font> + </family> + <family lang="und-Nbat"> + <font weight="400" style="normal">NotoSansNabataean-Regular.otf</font> + </family> + <family lang="und-Newa"> + <font weight="400" style="normal">NotoSansNewa-Regular.otf</font> + </family> + <family lang="und-Narb"> + <font weight="400" style="normal">NotoSansOldNorthArabian-Regular.otf</font> + </family> + <family lang="und-Perm"> + <font weight="400" style="normal">NotoSansOldPermic-Regular.otf</font> + </family> + <family lang="und-Hmng"> + <font weight="400" style="normal">NotoSansPahawhHmong-Regular.otf</font> + </family> + <family lang="und-Palm"> + <font weight="400" style="normal">NotoSansPalmyrene-Regular.otf</font> + </family> + <family lang="und-Pauc"> + <font weight="400" style="normal">NotoSansPauCinHau-Regular.otf</font> + </family> + <family lang="und-Shrd"> + <font weight="400" style="normal">NotoSansSharada-Regular.otf</font> + </family> + <family lang="und-Sora"> + <font weight="400" style="normal">NotoSansSoraSompeng-Regular.otf</font> + </family> + <family lang="und-Gong"> + <font weight="400" style="normal">NotoSansGunjalaGondi-Regular.otf</font> + </family> + <family lang="und-Rohg"> + <font weight="400" style="normal">NotoSansHanifiRohingya-Regular.otf</font> + </family> + <family lang="und-Khoj"> + <font weight="400" style="normal">NotoSansKhojki-Regular.otf</font> + </family> + <family lang="und-Gonm"> + <font weight="400" style="normal">NotoSansMasaramGondi-Regular.otf</font> + </family> + <family lang="und-Wcho"> + <font weight="400" style="normal">NotoSansWancho-Regular.otf</font> + </family> + <family lang="und-Wara"> + <font weight="400" style="normal">NotoSansWarangCiti-Regular.otf</font> + </family> + <family lang="und-Gran"> + <font weight="400" style="normal">NotoSansGrantha-Regular.ttf</font> + </family> + <family lang="und-Modi"> + <font weight="400" style="normal">NotoSansModi-Regular.ttf</font> + </family> + <family lang="und-Dogr"> + <font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font> + </family> + <family lang="und-Medf"> + <font weight="400" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Soyo"> + <font weight="400" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Takr"> + <font weight="400" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Hmnp"> + <font weight="400" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Yezi"> + <font weight="400" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> +</familyset> diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index f6ba103f6f05..ae61a2d811d2 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -17,6 +17,7 @@ package android.graphics; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; +import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION; import android.annotation.ColorInt; import android.annotation.ColorLong; @@ -133,7 +134,9 @@ public class Paint { FAKE_BOLD_TEXT_FLAG, LINEAR_TEXT_FLAG, SUBPIXEL_TEXT_FLAG, - EMBEDDED_BITMAP_TEXT_FLAG + EMBEDDED_BITMAP_TEXT_FLAG, + TEXT_RUN_FLAG_LEFT_EDGE, + TEXT_RUN_FLAG_RIGHT_EDGE }) public @interface PaintFlag{} @@ -264,6 +267,66 @@ public class Paint { /** @hide bit mask for the flag enabling vertical rendering for text */ public static final int VERTICAL_TEXT_FLAG = 0x1000; + /** + * A text run flag that indicates the run is located the visually most left segment of the line. + * <p> + * This flag is used for telling the underlying text layout engine that the text is located at + * the most left of the line. This flag is used for controlling the amount letter spacing + * added. If the text is in the middle of the line, the text layout engine assigns additional + * letter spacing to the both side of each letter. On the other hand, the letter spacing should + * not be added to the visually most left and right of the line. By setting this flag, text + * layout engine calculates the layout as it is located at the most visually left of the line + * and doesn't add letter spacing to the left of this run. + * <p> + * Note that the caller must resolve BiDi runs and reorder them visually and set this flag only + * if the target run is located visually most left position. This left does not always mean the + * beginning of the text. + * <p> + * If the run covers entire line, caller should set {@link #TEXT_RUN_FLAG_RIGHT_EDGE} as well. + * <p> + * Note that this flag is only effective for run based APIs. For example, this flag works for + * {@link Canvas#drawTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)} + * and + * {@link Paint#getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int)}. + * However, this doesn't work for + * {@link Canvas#drawText(CharSequence, int, int, float, float, Paint)} or + * {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both + * {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified. + */ + @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION) + public static final int TEXT_RUN_FLAG_LEFT_EDGE = 0x2000; + + + /** + * A text run flag that indicates the run is located the visually most right segment of the + * line. + * <p> + * This flag is used for telling the underlying text layout engine that the text is located at + * the most right of the line. This flag is used for controlling the amount letter spacing + * added. If the text is in the middle of the line, the text layout engine assigns additional + * letter spacing to the both side of each letter. On the other hand, the letter spacing should + * not be added to the visually most left and right of the line. By setting this flag, text + * layout engine calculates the layout as it is located at the most visually left of the line + * and doesn't add letter spacing to the left of this run. + * <p> + * Note that the caller must resolve BiDi runs and reorder them visually and set this flag only + * if the target run is located visually most right position. This right does not always mean + * the end of the text. + * <p> + * If the run covers entire line, caller should set {@link #TEXT_RUN_FLAG_LEFT_EDGE} as well. + * <p> + * Note that this flag is only effective for run based APIs. For example, this flag works for + * {@link Canvas#drawTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)} + * and + * {@link Paint#getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int)}. + * However, this doesn't work for + * {@link Canvas#drawText(CharSequence, int, int, float, float, Paint)} or + * {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both + * {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified. + */ + @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION) + public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 0x4000; + // These flags are always set on a new/reset paint, even if flags 0 is passed. static final int HIDDEN_DEFAULT_PAINT_FLAGS = DEV_KERN_TEXT_FLAG | EMBEDDED_BITMAP_TEXT_FLAG | FILTER_BITMAP_FLAG; @@ -2520,17 +2583,24 @@ public class Paint { if (text.length == 0 || count == 0) { return 0f; } - if (!mHasCompatScaling) { - return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, - index, count, index, count, mBidiFlags, null, 0)); - } + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count, - mBidiFlags, null, 0); - setTextSize(oldSize); - return (float) Math.ceil(w*mInvCompatScaling); + if (!mHasCompatScaling) { + return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, + index, count, index, count, mBidiFlags, null, 0)); + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); + final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count, + mBidiFlags, null, 0); + setTextSize(oldSize); + return (float) Math.ceil(w * mInvCompatScaling); + } finally { + setFlags(oldFlag); + } } /** @@ -2552,16 +2622,22 @@ public class Paint { if (text.length() == 0 || start == end) { return 0f; } - if (!mHasCompatScaling) { - return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, - start, end, start, end, mBidiFlags, null, 0)); + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + if (!mHasCompatScaling) { + return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, + start, end, start, end, mBidiFlags, null, 0)); + } + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); + final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, + null, 0); + setTextSize(oldSize); + return (float) Math.ceil(w * mInvCompatScaling); + } finally { + setFlags(oldFlag); } - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, - null, 0); - setTextSize(oldSize); - return (float) Math.ceil(w * mInvCompatScaling); } /** @@ -2766,19 +2842,26 @@ public class Paint { if (text.length == 0 || count == 0) { return 0; } - if (!mHasCompatScaling) { + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + if (!mHasCompatScaling) { + nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, + 0); + return count; + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0); + setTextSize(oldSize); + for (int i = 0; i < count; i++) { + widths[i] *= mInvCompatScaling; + } return count; + } finally { + setFlags(oldFlag); } - - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0); - setTextSize(oldSize); - for (int i = 0; i < count; i++) { - widths[i] *= mInvCompatScaling; - } - return count; } /** @@ -2849,19 +2932,25 @@ public class Paint { if (text.length() == 0 || start == end) { return 0; } - if (!mHasCompatScaling) { + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + if (!mHasCompatScaling) { + nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); + return end - start; + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); + setTextSize(oldSize); + for (int i = 0; i < end - start; i++) { + widths[i] *= mInvCompatScaling; + } return end - start; + } finally { + setFlags(oldFlag); } - - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); - setTextSize(oldSize); - for (int i = 0; i < end - start; i++) { - widths[i] *= mInvCompatScaling; - } - return end - start; } /** diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java index ddae673e1084..b21bf11088e2 100644 --- a/graphics/java/android/graphics/text/LineBreakConfig.java +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -23,9 +23,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.compat.CompatChanges; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; +import android.app.ActivityThread; import android.os.Build; import android.os.LocaleList; import android.os.Parcel; @@ -43,15 +41,6 @@ import java.util.Objects; * line-break property</a> for more information. */ public final class LineBreakConfig implements Parcelable { - - /** - * A feature ID for automatic line break word style. - * @hide - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) - public static final long WORD_STYLE_AUTO = 280005585L; - /** * No hyphenation preference is specified. * @@ -487,8 +476,15 @@ public final class LineBreakConfig implements Parcelable { * @hide */ public static @LineBreakStyle int getResolvedLineBreakStyle(@Nullable LineBreakConfig config) { - final int defaultStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO) - ? LINE_BREAK_STYLE_AUTO : LINE_BREAK_STYLE_NONE; + final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo() + .targetSdkVersion; + final int defaultStyle; + final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM; + if (targetSdkVersion >= vicVersion) { + defaultStyle = LINE_BREAK_STYLE_AUTO; + } else { + defaultStyle = LINE_BREAK_STYLE_NONE; + } if (config == null) { return defaultStyle; } @@ -515,8 +511,15 @@ public final class LineBreakConfig implements Parcelable { */ public static @LineBreakWordStyle int getResolvedLineBreakWordStyle( @Nullable LineBreakConfig config) { - final int defaultWordStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO) - ? LINE_BREAK_WORD_STYLE_AUTO : LINE_BREAK_WORD_STYLE_NONE; + final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo() + .targetSdkVersion; + final int defaultWordStyle; + final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM; + if (targetSdkVersion >= vicVersion) { + defaultWordStyle = LINE_BREAK_WORD_STYLE_AUTO; + } else { + defaultWordStyle = LINE_BREAK_WORD_STYLE_NONE; + } if (config == null) { return defaultWordStyle; } diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java index 2d33e8d24ece..6da07198c3ad 100644 --- a/graphics/java/android/graphics/text/MeasuredText.java +++ b/graphics/java/android/graphics/text/MeasuredText.java @@ -269,6 +269,10 @@ public class MeasuredText { * offset is zero. After the style is applied the internal offset is moved to {@code offset * + length}, and next call will start from this new position. * + * <p> + * {@link Paint#TEXT_RUN_FLAG_RIGHT_EDGE} and {@link Paint#TEXT_RUN_FLAG_LEFT_EDGE} are + * ignored and treated as both of them are set. + * * @param paint a paint * @param length a length to be applied with a given paint, can not exceed the length of the * text diff --git a/keystore/java/android/security/Authorization.java b/keystore/java/android/security/Authorization.java index 4ec5e1b67c5d..6404c4bc33d6 100644 --- a/keystore/java/android/security/Authorization.java +++ b/keystore/java/android/security/Authorization.java @@ -100,12 +100,14 @@ public class Authorization { * * @param userId - the user's Android user ID * @param unlockingSids - list of biometric SIDs with which the device may be unlocked again + * @param weakUnlockEnabled - true if non-strong biometric or trust agent unlock is enabled * @return 0 if successful or a {@code ResponseCode}. */ - public static int onDeviceLocked(int userId, @NonNull long[] unlockingSids) { + public static int onDeviceLocked(int userId, @NonNull long[] unlockingSids, + boolean weakUnlockEnabled) { StrictMode.noteDiskWrite(); try { - getService().onDeviceLocked(userId, unlockingSids); + getService().onDeviceLocked(userId, unlockingSids, weakUnlockEnabled); return 0; } catch (RemoteException | NullPointerException e) { Log.w(TAG, "Can not connect to keystore", e); diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 45540e0fbbb8..4cdc06a999a7 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -162,6 +162,7 @@ android_library { "com_android_wm_shell_flags_lib", "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", + "perfetto_trace_java_protos", "dagger2", "jsr330", ], diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 901d5fa0cd9a..0e046581cb48 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -1,13 +1,6 @@ package: "com.android.wm.shell" flag { - name: "example_flag" - namespace: "multitasking" - description: "An Example Flag" - bug: "300136750" -} - -flag { name: "enable_app_pairs" namespace: "multitasking" description: "Enables the ability to create and save app pairs to the Home screen" diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index e4abae48c8fd..9854e58dd7cf 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -134,6 +134,13 @@ <!-- Whether the additional education about reachability is enabled --> <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool> + <!-- The minimum tolerance of the percentage of activity bounds within its task to hide + size compat restart button. Value lower than 0 or higher than 100 will be ignored. + 100 is the default value where the activity has to fit exactly within the task to allow + size compat restart button to be hidden. 0 means size compat restart button will always + be hidden. --> + <integer name="config_letterboxRestartButtonHideTolerance">100</integer> + <!-- Whether DragAndDrop capability is enabled --> <bool name="config_enableShellDragDrop">true</bool> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index bb433dbbd2ce..e7f6f0d61847 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -17,7 +17,7 @@ package com.android.wm.shell.back; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; -import static com.android.window.flags.Flags.predictiveBackSystemAnimations; +import static com.android.window.flags.Flags.predictiveBackSystemAnims; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; @@ -244,7 +244,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void setupAnimationDeveloperSettingsObserver( @NonNull ContentResolver contentResolver, @NonNull @ShellBackgroundThread final Handler backgroundHandler) { - if (predictiveBackSystemAnimations()) { + if (predictiveBackSystemAnims()) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore " + "developer settings flag is ignored and no content observer registered"); return; @@ -267,7 +267,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ @ShellBackgroundThread private void updateEnableAnimationFromFlags() { - boolean isEnabled = predictiveBackSystemAnimations() || isDeveloperSettingEnabled(); + boolean isEnabled = predictiveBackSystemAnims() || isDeveloperSettingEnabled(); mEnableAnimations.set(isEnabled); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index e48732801094..bb0dd95b042f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -769,8 +769,10 @@ public class StackAnimationController extends boolean swapped = false; for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { View view = bubbleViews.get(newIndex); - final int oldIndex = mLayout.indexOfChild(view); - swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + if (view != null) { + final int oldIndex = mLayout.indexOfChild(view); + swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + } } if (!swapped) { // All bubbles were at the right position. Make sure badges and z order is correct. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 12114519d086..bd8ce803c591 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -26,6 +26,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; import android.view.TouchDelegate; import android.view.View; import android.view.ViewTreeObserver; @@ -74,10 +75,6 @@ public class BubbleBarLayerView extends FrameLayout private DismissView mDismissView; private @Nullable Consumer<String> mUnBubbleConversationCallback; - // TODO(b/273310265) - currently the view is always on the right, need to update for RTL. - /** Whether the expanded view is displaying on the left of the screen or not. */ - private boolean mOnLeft = false; - /** Whether a bubble is expanded. */ private boolean mIsExpanded = false; @@ -154,10 +151,10 @@ public class BubbleBarLayerView extends FrameLayout return mIsExpanded; } - // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done. + // TODO(b/313661121) - when dragging is implemented, check user setting first /** Whether the expanded view is positioned on the left or right side of the screen. */ public boolean isOnLeft() { - return mOnLeft; + return getLayoutDirection() == LAYOUT_DIRECTION_RTL; } /** Shows the expanded view of the provided bubble. */ @@ -216,7 +213,7 @@ public class BubbleBarLayerView extends FrameLayout return Unit.INSTANCE; }); - addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); + addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT)); } if (mEducationViewController.isEducationVisible()) { @@ -311,7 +308,7 @@ public class BubbleBarLayerView extends FrameLayout lp.width = width; lp.height = height; mExpandedView.setLayoutParams(lp); - if (mOnLeft) { + if (isOnLeft()) { mExpandedView.setX(mPositioner.getInsets().left + padding); } else { mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java index 09d99b204bdb..fa2e23647a39 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java @@ -71,6 +71,8 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi private static final String HAS_SEEN_VERTICAL_REACHABILITY_EDUCATION_KEY_PREFIX = "has_seen_vertical_reachability_education"; + private static final int MAX_PERCENTAGE_VAL = 100; + /** * The {@link SharedPreferences} instance for the restart dialog and the reachability * education. @@ -82,6 +84,12 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi */ private final SharedPreferences mLetterboxEduSharedPreferences; + /** + * The minimum tolerance of the percentage of activity bounds within its task to hide + * size compat restart button. + */ + private final int mHideSizeCompatRestartButtonTolerance; + // Whether the extended restart dialog is enabled private boolean mIsRestartDialogEnabled; @@ -106,6 +114,9 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi R.bool.config_letterboxIsRestartDialogEnabled); mIsReachabilityEducationEnabled = context.getResources().getBoolean( R.bool.config_letterboxIsReachabilityEducationEnabled); + final int tolerance = context.getResources().getInteger( + R.integer.config_letterboxRestartButtonHideTolerance); + mHideSizeCompatRestartButtonTolerance = getHideSizeCompatRestartButtonTolerance(tolerance); mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG); @@ -179,6 +190,10 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi || !hasSeenVerticalReachabilityEducation(taskInfo)); } + int getHideSizeCompatRestartButtonTolerance() { + return mHideSizeCompatRestartButtonTolerance; + } + boolean getHasSeenLetterboxEducation(int userId) { return mLetterboxEduSharedPreferences .getBoolean(dontShowLetterboxEduKey(userId), /* default= */ false); @@ -218,6 +233,15 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi } } + // Returns the minimum tolerance of the percentage of activity bounds within its task to hide + // size compat restart button. Value lower than 0 or higher than 100 will be ignored. + // 100 is the default value where the activity has to fit exactly within the task to allow + // size compat restart button to be hidden. 0 means size compat restart button will always + // be hidden. + private int getHideSizeCompatRestartButtonTolerance(int tolerance) { + return tolerance < 0 || tolerance > MAX_PERCENTAGE_VAL ? MAX_PERCENTAGE_VAL : tolerance; + } + private boolean isReachabilityEducationEnabled() { return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled && mIsLetterboxReachabilityEducationAllowed); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 00e0cdb034b6..2dd27430e348 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -22,7 +22,9 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPL import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AppCompatTaskInfo; import android.app.AppCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.Context; @@ -33,6 +35,7 @@ import android.view.LayoutInflater; import android.view.View; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; @@ -68,6 +71,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @VisibleForTesting CompatUILayout mLayout; + private final float mHideScmTolerance; + CompatUIWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, CompatUICallback callback, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, @@ -75,11 +80,13 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat + && shouldShowSizeCompatRestartButton(taskInfo); mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; mCompatUIConfiguration = compatUIConfiguration; mOnRestartButtonClicked = onRestartButtonClicked; + mHideScmTolerance = mCompatUIConfiguration.getHideSizeCompatRestartButtonTolerance(); } @Override @@ -107,6 +114,11 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mLayout = inflateLayout(); mLayout.inject(this); + final TaskInfo taskInfo = getLastTaskInfo(); + if (taskInfo != null) { + mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo); + } + updateVisibilityOfViews(); if (mHasSizeCompat) { @@ -127,7 +139,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { boolean canShow) { final boolean prevHasSizeCompat = mHasSizeCompat; final int prevCameraCompatControlState = mCameraCompatControlState; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat + && shouldShowSizeCompatRestartButton(taskInfo); mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { @@ -208,6 +221,30 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { updateSurfacePosition(positionX, positionY); } + @VisibleForTesting + boolean shouldShowSizeCompatRestartButton(@NonNull TaskInfo taskInfo) { + if (!Flags.allowHideScmButton()) { + return true; + } + final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + final int letterboxArea = computeArea(appCompatTaskInfo.topActivityLetterboxWidth, + appCompatTaskInfo.topActivityLetterboxHeight); + final int taskArea = computeArea(taskBounds.width(), taskBounds.height()); + if (letterboxArea == 0 || taskArea == 0) { + return false; + } + final float percentageAreaOfLetterboxInTask = (float) letterboxArea / taskArea * 100; + return percentageAreaOfLetterboxInTask < mHideScmTolerance; + } + + private int computeArea(int width, int height) { + if (width == 0 || height == 0) { + return 0; + } + return width * height; + } + private void updateVisibilityOfViews() { if (mLayout == null) { return; 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 95d7ad5c416f..c3a82ce258df 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 @@ -99,7 +99,11 @@ class DragToDesktopTransitionHandler( windowDecoration: DesktopModeWindowDecoration ) { if (inProgress) { - error("A drag to desktop is already in progress") + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DragToDesktop: Drag to desktop transition already in progress." + ) + return } val options = ActivityOptions.makeBasic().apply { @@ -144,6 +148,12 @@ class DragToDesktopTransitionHandler( * inside the desktop drop zone. */ fun finishDragToDesktopTransition(wct: WindowContainerTransaction) { + if (!inProgress) { + // Don't attempt to finish a drag to desktop transition since there is no transition in + // progress which means that the drag to desktop transition was never successfully + // started. + return + } if (requireTransitionState().startAborted) { // Don't attempt to complete the drag-to-desktop since the start transition didn't // succeed as expected. Just reset the state as if nothing happened. @@ -161,6 +171,12 @@ class DragToDesktopTransitionHandler( * means the user wants to remain in their current windowing mode. */ fun cancelDragToDesktopTransition() { + if (!inProgress) { + // Don't attempt to cancel a drag to desktop transition since there is no transition in + // progress which means that the drag to desktop transition was never successfully + // started. + return + } val state = requireTransitionState() if (state.startAborted) { // Don't attempt to cancel the drag-to-desktop since the start transition didn't 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 b0d8b47b170a..3fb0dbfaa63d 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 @@ -83,6 +83,9 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.tracing.LegacyTransitionTracer; +import com.android.wm.shell.transition.tracing.PerfettoTransitionTracer; +import com.android.wm.shell.transition.tracing.TransitionTracer; import com.android.wm.shell.util.TransitionUtil; import java.io.PrintWriter; @@ -184,7 +187,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); private final SleepHandler mSleepHandler = new SleepHandler(); - private final Tracer mTracer = new Tracer(); + private final TransitionTracer mTransitionTracer; private boolean mIsRegistered = false; /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ @@ -307,6 +310,12 @@ public class Transitions implements RemoteCallable<Transitions>, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); shellInit.addInitCallback(this::onInit, this); mHomeTransitionObserver = observer; + + if (android.tracing.Flags.perfettoTransitionTracing()) { + mTransitionTracer = new PerfettoTransitionTracer(); + } else { + mTransitionTracer = new LegacyTransitionTracer(); + } } private void onInit() { @@ -868,7 +877,7 @@ public class Transitions implements RemoteCallable<Transitions>, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" + " %s is still animating. Notify the animating transition" + " in case they can be merged", ready, playing); - mTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); + mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, playing.mToken, (wct) -> onMerged(playing, ready)); } @@ -902,7 +911,7 @@ public class Transitions implements RemoteCallable<Transitions>, for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken); } - mTracer.logMerged(merged.mInfo.getDebugId(), playing.mInfo.getDebugId()); + mTransitionTracer.logMerged(merged.mInfo.getDebugId(), playing.mInfo.getDebugId()); // See if we should merge another transition. processReadyQueue(track); } @@ -923,7 +932,7 @@ public class Transitions implements RemoteCallable<Transitions>, active.mStartT, active.mFinishT, (wct) -> onFinish(active, wct)); if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); - mTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); + mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); return; } } @@ -948,7 +957,7 @@ public class Transitions implements RemoteCallable<Transitions>, if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s", mHandlers.get(i)); - mTracer.logDispatched(info.getDebugId(), mHandlers.get(i)); + mTransitionTracer.logDispatched(info.getDebugId(), mHandlers.get(i)); return mHandlers.get(i); } } @@ -978,7 +987,7 @@ public class Transitions implements RemoteCallable<Transitions>, final Track track = mTracks.get(transition.getTrack()); transition.mAborted = true; - mTracer.logAborted(transition.mInfo.getDebugId()); + mTransitionTracer.logAborted(transition.mInfo.getDebugId()); if (transition.mHandler != null) { // Notifies to clean-up the aborted transition. @@ -1506,12 +1515,18 @@ public class Transitions implements RemoteCallable<Transitions>, } } - @Override public boolean onShellCommand(String[] args, PrintWriter pw) { switch (args[0]) { case "tracing": { - mTracer.onShellCommand(Arrays.copyOfRange(args, 1, args.length), pw); + if (!android.tracing.Flags.perfettoTransitionTracing()) { + ((LegacyTransitionTracer) mTransitionTracer) + .onShellCommand(Arrays.copyOfRange(args, 1, args.length), pw); + } else { + pw.println("Command not supported. Use the Perfetto command instead to start " + + "and stop this trace instead."); + return false; + } return true; } default: { @@ -1524,8 +1539,10 @@ public class Transitions implements RemoteCallable<Transitions>, @Override public void printShellCommandHelp(PrintWriter pw, String prefix) { - pw.println(prefix + "tracing"); - mTracer.printShellCommandHelp(pw, prefix + " "); + if (!android.tracing.Flags.perfettoTransitionTracing()) { + pw.println(prefix + "tracing"); + ((LegacyTransitionTracer) mTransitionTracer).printShellCommandHelp(pw, prefix + " "); + } } private void dump(@NonNull PrintWriter pw, String prefix) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/LegacyTransitionTracer.java index 5919aad133c7..9c848869e0f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/LegacyTransitionTracer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.transition; +package com.android.wm.shell.transition.tracing; import static android.os.Build.IS_USER; @@ -29,6 +29,7 @@ import android.util.Log; import com.android.internal.util.TraceBuffer; import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.transition.Transitions; import com.google.protobuf.nano.MessageNano; @@ -45,7 +46,8 @@ import java.util.concurrent.TimeUnit; /** * Helper class to collect and dump transition traces. */ -public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { +public class LegacyTransitionTracer + implements ShellCommandHandler.ShellCommandActionHandler, TransitionTracer { private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB @@ -60,33 +62,33 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { private final TraceBuffer.ProtoProvider mProtoProvider = new TraceBuffer.ProtoProvider<MessageNano, - com.android.wm.shell.nano.WmShellTransitionTraceProto, - com.android.wm.shell.nano.Transition>() { - @Override - public int getItemSize(MessageNano proto) { - return proto.getCachedSize(); - } - - @Override - public byte[] getBytes(MessageNano proto) { - return MessageNano.toByteArray(proto); - } - - @Override - public void write( - com.android.wm.shell.nano.WmShellTransitionTraceProto encapsulatingProto, - Queue<com.android.wm.shell.nano.Transition> buffer, OutputStream os) + com.android.wm.shell.nano.WmShellTransitionTraceProto, + com.android.wm.shell.nano.Transition>() { + @Override + public int getItemSize(MessageNano proto) { + return proto.getCachedSize(); + } + + @Override + public byte[] getBytes(MessageNano proto) { + return MessageNano.toByteArray(proto); + } + + @Override + public void write( + com.android.wm.shell.nano.WmShellTransitionTraceProto encapsulatingProto, + Queue<com.android.wm.shell.nano.Transition> buffer, OutputStream os) throws IOException { - encapsulatingProto.transitions = buffer.toArray( - new com.android.wm.shell.nano.Transition[0]); - os.write(getBytes(encapsulatingProto)); - } - }; + encapsulatingProto.transitions = buffer.toArray( + new com.android.wm.shell.nano.Transition[0]); + os.write(getBytes(encapsulatingProto)); + } + }; private final TraceBuffer<MessageNano, com.android.wm.shell.nano.WmShellTransitionTraceProto, - com.android.wm.shell.nano.Transition> mTraceBuffer - = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY, mProtoProvider, - (proto) -> handleOnEntryRemovedFromTrace(proto)); + com.android.wm.shell.nano.Transition> mTraceBuffer = + new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY, mProtoProvider, + this::handleOnEntryRemovedFromTrace); private final Map<Object, Runnable> mRemovedFromTraceCallbacks = new HashMap<>(); private final Map<Transitions.TransitionHandler, Integer> mHandlerIds = new HashMap<>(); @@ -99,6 +101,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * @param transitionId The id of the transition being dispatched. * @param handler The handler the transition is being dispatched to. */ + @Override public void logDispatched(int transitionId, Transitions.TransitionHandler handler) { final int handlerId; if (mHandlerIds.containsKey(handler)) { @@ -130,6 +133,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged. */ + @Override public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition(); proto.id = mergeRequestedTransitionId; @@ -145,6 +149,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * @param mergedTransitionId The id of the transition that was merged. * @param playingTransitionId The id of the transition the transition was merged into. */ + @Override public void logMerged(int mergedTransitionId, int playingTransitionId) { com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition(); proto.id = mergedTransitionId; @@ -159,6 +164,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * * @param transitionId The id of the transition that was aborted. */ + @Override public void logAborted(int transitionId) { com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition(); proto.id = transitionId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java new file mode 100644 index 000000000000..99df6a31beac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java @@ -0,0 +1,174 @@ +/* + * 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.wm.shell.transition.tracing; + +import android.internal.perfetto.protos.PerfettoTrace; +import android.os.SystemClock; +import android.tracing.perfetto.DataSourceInstance; +import android.tracing.perfetto.DataSourceParams; +import android.tracing.perfetto.InitArguments; +import android.tracing.perfetto.Producer; +import android.tracing.perfetto.TracingContext; +import android.tracing.transition.TransitionDataSource; +import android.util.proto.ProtoOutputStream; + +import com.android.wm.shell.transition.Transitions; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Helper class to collect and dump transition traces. + */ +public class PerfettoTransitionTracer implements TransitionTracer { + private final AtomicInteger mActiveTraces = new AtomicInteger(0); + private final TransitionDataSource mDataSource = new TransitionDataSource( + mActiveTraces::incrementAndGet, + this::onFlush, + mActiveTraces::decrementAndGet); + + public PerfettoTransitionTracer() { + Producer.init(InitArguments.DEFAULTS); + mDataSource.register(DataSourceParams.DEFAULTS); + } + + /** + * Adds an entry in the trace to log that a transition has been dispatched to a handler. + * + * @param transitionId The id of the transition being dispatched. + * @param handler The handler the transition is being dispatched to. + */ + @Override + public void logDispatched(int transitionId, Transitions.TransitionHandler handler) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final int handlerId = getHandlerId(handler, ctx); + + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, transitionId); + os.write(PerfettoTrace.ShellTransition.DISPATCH_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.write(PerfettoTrace.ShellTransition.HANDLER, handlerId); + os.end(token); + }); + } + + private static int getHandlerId(Transitions.TransitionHandler handler, + TracingContext<DataSourceInstance, TransitionDataSource.TlsState, Void> ctx) { + final Map<String, Integer> handlerMapping = + ctx.getCustomTlsState().handlerMapping; + final int handlerId; + if (handlerMapping.containsKey(handler.getClass().getName())) { + handlerId = handlerMapping.get(handler.getClass().getName()); + } else { + // + 1 to avoid 0 ids which can be confused with missing value when dumped to proto + handlerId = handlerMapping.size() + 1; + handlerMapping.put(handler.getClass().getName(), handlerId); + } + return handlerId; + } + + /** + * Adds an entry in the trace to log that a request to merge a transition was made. + * + * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged. + */ + @Override + public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); + os.write(PerfettoTrace.ShellTransition.MERGE_REQUEST_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.end(token); + }); + } + + /** + * Adds an entry in the trace to log that a transition was merged by the handler. + * + * @param mergedTransitionId The id of the transition that was merged. + * @param playingTransitionId The id of the transition the transition was merged into. + */ + @Override + public void logMerged(int mergedTransitionId, int playingTransitionId) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, mergedTransitionId); + os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.end(token); + }); + } + + /** + * Adds an entry in the trace to log that a transition was aborted. + * + * @param transitionId The id of the transition that was aborted. + */ + @Override + public void logAborted(int transitionId) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, transitionId); + os.write(PerfettoTrace.ShellTransition.SHELL_ABORT_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.end(token); + }); + } + + private boolean isTracing() { + return mActiveTraces.get() > 0; + } + + private void onFlush() { + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + + final Map<String, Integer> handlerMapping = ctx.getCustomTlsState().handlerMapping; + for (String handler : handlerMapping.keySet()) { + final long token = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS); + os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerMapping.get(handler)); + os.write(PerfettoTrace.ShellHandlerMapping.NAME, handler); + os.end(token); + } + + ctx.flush(); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java new file mode 100644 index 000000000000..5857ad88e9e6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java @@ -0,0 +1,51 @@ +/* + * 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.wm.shell.transition.tracing; + +import com.android.wm.shell.transition.Transitions; + +public interface TransitionTracer { + /** + * Adds an entry in the trace to log that a transition has been dispatched to a handler. + * + * @param transitionId The id of the transition being dispatched. + * @param handler The handler the transition is being dispatched to. + */ + void logDispatched(int transitionId, Transitions.TransitionHandler handler); + + /** + * Adds an entry in the trace to log that a request to merge a transition was made. + * + * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged. + */ + void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId); + + /** + * Adds an entry in the trace to log that a transition was merged by the handler. + * + * @param mergedTransitionId The id of the transition that was merged. + * @param playingTransitionId The id of the transition the transition was merged into. + */ + void logMerged(int mergedTransitionId, int playingTransitionId); + + /** + * Adds an entry in the trace to log that a transition was aborted. + * + * @param transitionId The id of the transition that was aborted. + */ + void logAborted(int transitionId); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java index 4aed7c449750..e6d35e83116b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java @@ -16,8 +16,6 @@ package com.android.wm.shell.unfold; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; - import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; @@ -230,8 +228,7 @@ public class UnfoldAnimationController implements UnfoldListener { } private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { - // TODO(b/311084698): the windowing mode check is added here as a work around. - if (!mIsInStageChange || taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { + if (!mIsInStageChange) { // No need to resetTask if there is no ongoing state change. return; } 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 1a793a16f254..b2eeea7048bc 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 @@ -271,6 +271,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (e.findPointerIndex(mDragPointerId) == -1) { mDragPointerId = e.getPointerId(0); } + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + // If a decor's resize drag zone is active, don't also try to reposition it. + if (decoration.isHandlingDragResize()) break; final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); 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 1debb02e86af..5a74255df49a 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 @@ -286,6 +286,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL closeBackground.setTintList(buttonTintColor); } + boolean isHandlingDragResize() { + return mDragResizeListener.isHandlingDragResize(); + } + private void closeDragResizeListener() { if (mDragResizeListener == null) { return; 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 aabc1cfb5875..554b1fb99550 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 @@ -491,8 +491,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } case MotionEvent.ACTION_MOVE: { + mShouldClick = false; final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + // If a decor's resize drag zone is active, don't also try to reposition it. + if (decoration.isHandlingDragResize()) break; decoration.closeMaximizeMenu(); if (e.findPointerIndex(mDragPointerId) == -1) { mDragPointerId = e.getPointerId(0); @@ -505,7 +508,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { e.getRawX(dragPointerIdx), newTaskBounds)); mIsDragging = true; - mShouldClick = false; return true; } case MotionEvent.ACTION_UP: 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 53f806ccabee..20233331997f 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 @@ -387,6 +387,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mHandleMenu != null; } + boolean isHandlingDragResize() { + return mDragResizeListener.isHandlingDragResize(); + } + private void loadAppInfo() { String packageName = mTaskInfo.realActivity.getPackageName(); PackageManager pm = mContext.getApplicationContext().getPackageManager(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 8511a21d4294..8b38f991a2db 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -320,6 +320,10 @@ class DragResizeInputListener implements AutoCloseable { } } + boolean isHandlingDragResize() { + return mInputEventReceiver.isHandlingEvents(); + } + @Override public void close() { mInputEventReceiver.dispose(); @@ -386,6 +390,10 @@ class DragResizeInputListener implements AutoCloseable { finishInputEvent(inputEvent, handleInputEvent(inputEvent)); } + boolean isHandlingEvents() { + return mShouldHandleEvents; + } + private boolean handleInputEvent(InputEvent inputEvent) { if (!(inputEvent instanceof MotionEvent)) { return false; @@ -409,7 +417,6 @@ class DragResizeInputListener implements AutoCloseable { mShouldHandleEvents = isInResizeHandleBounds(x, y); } if (mShouldHandleEvents) { - mInputManager.pilferPointers(mInputChannel.getToken()); mDragPointerId = e.getPointerId(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); @@ -427,6 +434,7 @@ class DragResizeInputListener implements AutoCloseable { if (!mShouldHandleEvents) { break; } + mInputManager.pilferPointers(mInputChannel.getToken()); int dragPointerIndex = e.findPointerIndex(mDragPointerId); float rawX = e.getRawX(dragPointerIndex); float rawY = e.getRawY(dragPointerIndex); @@ -437,6 +445,7 @@ class DragResizeInputListener implements AutoCloseable { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { + mInputManager.pilferPointers(mInputChannel.getToken()); if (mShouldHandleEvents) { int dragPointerIndex = e.findPointerIndex(mDragPointerId); final Rect taskBounds = mCallback.onDragPositioningEnd( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index c1b18f959641..7c6e69eb1ec9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -61,6 +61,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, private int mCtrlType; private boolean mIsResizingOrAnimatingResize; @Surface.Rotation private int mRotation; + private boolean mVeilIsVisible; public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, @@ -94,7 +95,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); mRepositionStartPoint.set(x, y); if (isResizing()) { - mDesktopWindowDecoration.showResizeVeil(mTaskBoundsAtDragStart); if (!mDesktopWindowDecoration.mTaskInfo.isFocused) { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); @@ -119,8 +119,13 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType, mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, mDisplayController, mDesktopWindowDecoration)) { - mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); mIsResizingOrAnimatingResize = true; + if (!mVeilIsVisible) { + mDesktopWindowDecoration.showResizeVeil(mRepositionTaskBounds); + mVeilIsVisible = true; + } else { + mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); + } } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { final SurfaceControl.Transaction t = mTransactionSupplier.get(); DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration, @@ -143,7 +148,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); mTransitions.startTransition(TRANSIT_CHANGE, wct, this); - } else { + } else if (mVeilIsVisible) { // If bounds haven't changed, perform necessary veil reset here as startAnimation // won't be called. mDesktopWindowDecoration.hideResizeVeil(); @@ -163,6 +168,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mCtrlType = CTRL_TYPE_UNDEFINED; mTaskBoundsAtDragStart.setEmpty(); mRepositionStartPoint.set(0, 0); + mVeilIsVisible = false; return new Rect(mRepositionTaskBounds); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index d4b97ed55192..2acfd83084ab 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -20,6 +20,8 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsets.Type.navigationBars; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -27,6 +29,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; @@ -39,6 +42,7 @@ import android.app.AppCompatTaskInfo; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.DisplayInfo; @@ -50,6 +54,7 @@ import android.view.View; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; @@ -59,6 +64,7 @@ import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -76,6 +82,8 @@ import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @SmallTest public class CompatUIWindowManagerTest extends ShellTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); private static final int TASK_ID = 1; @@ -107,6 +115,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public void testCreateSizeCompatButton() { // Doesn't create layout if show is false. mWindowManager.mHasSizeCompat = true; + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); assertTrue(mWindowManager.createLayout(/* canShow= */ false)); verify(mWindowManager, never()).inflateLayout(); @@ -199,6 +208,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // No diff clearInvocations(mWindowManager); TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true)); verify(mWindowManager, never()).updateSurfacePosition(); @@ -284,6 +294,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testUpdateCompatInfoLayoutNotInflatedYet() { mWindowManager.mHasSizeCompat = true; + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); mWindowManager.createLayout(/* canShow= */ false); verify(mWindowManager, never()).inflateLayout(); @@ -353,6 +364,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Create button if it is not created. mWindowManager.mLayout = null; mWindowManager.mHasSizeCompat = true; + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); mWindowManager.updateVisibility(/* canShow= */ true); verify(mWindowManager).createLayout(/* canShow= */ true); @@ -464,6 +476,37 @@ public class CompatUIWindowManagerTest extends ShellTestCase { Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); } + @Test + public void testShouldShowSizeCompatRestartButton() { + mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON); + + doReturn(86).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); + mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, + mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mCompatUIConfiguration, mOnRestartButtonClicked); + + // Simulate rotation of activity in square display + TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000; + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850; + + assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + + // Simulate exiting split screen/folding + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + + // Simulate folding + taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000)); + assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 500; + assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + } + private static TaskInfo createTaskInfo(boolean hasSizeCompat, @AppCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); 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 3bc90ade898e..be639e867e0b 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 @@ -34,6 +34,7 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever @@ -150,6 +151,23 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + fun startDragToDesktop_anotherTransitionInProgress_startDropped() { + val task = createTask() + val dragAnimator = mock<MoveToDesktopAnimator>() + + // Simulate attempt to start two drag to desktop transitions. + startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(task, dragAnimator) + + // Verify transition only started once. + verify(transitions, times(1)).startTransition( + eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP), + any(), + eq(handler) + ) + } + + @Test fun cancelDragToDesktop_startWasReady_cancel() { val task = createTask() val dragAnimator = mock<MoveToDesktopAnimator>() @@ -189,6 +207,32 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { verifyZeroInteractions(dragAnimator) } + @Test + fun cancelDragToDesktop_transitionNotInProgress_dropCancel() { + // Then cancel is called before the transition was started. + handler.cancelDragToDesktopTransition() + + // Verify cancel is dropped. + verify(transitions, never()).startTransition( + eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), + any(), + eq(handler) + ) + } + + @Test + fun finishDragToDesktop_transitionNotInProgress_dropFinish() { + // Then finish is called before the transition was started. + handler.finishDragToDesktopTransition(WindowContainerTransaction()) + + // Verify finish is dropped. + verify(transitions, never()).startTransition( + eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), + any(), + eq(handler) + ) + } + private fun startDragToDesktopTransition( task: RunningTaskInfo, dragAnimator: MoveToDesktopAnimator diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 08412101c30c..86253f35a51d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -144,13 +144,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_noMove_showsResizeVeil() { + fun testDragResize_noMove_doesNotShowResizeVeil() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) + verify(mockDesktopWindowDecoration, never()).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningEnd( STARTING_BOUNDS.left.toFloat(), @@ -162,7 +162,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && change.configuration.windowConfiguration.bounds == STARTING_BOUNDS}}, eq(taskPositioner)) - verify(mockDesktopWindowDecoration).hideResizeVeil() + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() } @Test @@ -212,7 +212,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.right.toFloat() + 10, @@ -222,6 +221,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { val rectAfterMove = Rect(STARTING_BOUNDS) rectAfterMove.right += 10 rectAfterMove.top += 10 + verify(mockDesktopWindowDecoration).showResizeVeil(rectAfterMove) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && @@ -237,7 +237,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { val rectAfterEnd = Rect(rectAfterMove) rectAfterEnd.right += 10 rectAfterEnd.top += 10 - verify(mockDesktopWindowDecoration, times(2)).updateResizeVeil(any()) + verify(mockDesktopWindowDecoration).updateResizeVeil(any()) verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && @@ -253,7 +253,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.left.toFloat(), diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index c9d5e074271b..d9166a16cdea 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -21,6 +21,7 @@ #include <algorithm> #include <cstddef> #include <limits> +#include <optional> #include "android-base/logging.h" #include "android-base/stringprintf.h" @@ -50,7 +51,9 @@ namespace { // contiguous block of memory to store both the TypeSpec struct and // the Type structs. struct TypeSpecBuilder { - explicit TypeSpecBuilder(incfs::verified_map_ptr<ResTable_typeSpec> header) : header_(header) {} + explicit TypeSpecBuilder(incfs::verified_map_ptr<ResTable_typeSpec> header) : header_(header) { + type_entries.reserve(dtohs(header_->typesCount)); + } void AddType(incfs::verified_map_ptr<ResTable_type> type) { TypeSpec::TypeEntry& entry = type_entries.emplace_back(); @@ -59,6 +62,7 @@ struct TypeSpecBuilder { } TypeSpec Build() { + type_entries.shrink_to_fit(); return {header_, std::move(type_entries)}; } @@ -450,7 +454,8 @@ const LoadedPackage* LoadedArsc::GetPackageById(uint8_t package_id) const { std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, package_property_t property_flags) { ATRACE_NAME("LoadedPackage::Load"); - std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage()); + const bool optimize_name_lookups = (property_flags & PROPERTY_OPTIMIZE_NAME_LOOKUPS) != 0; + std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage(optimize_name_lookups)); // typeIdOffset was added at some point, but we still must recognize apps built before this // was added. @@ -499,7 +504,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, // A map of TypeSpec builders, each associated with an type index. // We use these to accumulate the set of Types available for a TypeSpec, and later build a single, // contiguous block of memory that holds all the Types together with the TypeSpec. - std::unordered_map<int, std::unique_ptr<TypeSpecBuilder>> type_builder_map; + std::unordered_map<int, std::optional<TypeSpecBuilder>> type_builder_map; ChunkIterator iter(chunk.data_ptr(), chunk.data_size()); while (iter.HasNext()) { @@ -567,14 +572,14 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, return {}; } - if (entry_count * sizeof(uint32_t) > chunk.data_size()) { + if (entry_count * sizeof(uint32_t) > child_chunk.data_size()) { LOG(ERROR) << "RES_TABLE_TYPE_SPEC_TYPE too small to hold entries."; return {}; } - std::unique_ptr<TypeSpecBuilder>& builder_ptr = type_builder_map[type_spec->id]; - if (builder_ptr == nullptr) { - builder_ptr = util::make_unique<TypeSpecBuilder>(type_spec.verified()); + auto& maybe_type_builder = type_builder_map[type_spec->id]; + if (!maybe_type_builder) { + maybe_type_builder.emplace(type_spec.verified()); loaded_package->resource_ids_.set(type_spec->id, entry_count); } else { LOG(WARNING) << StringPrintf("RES_TABLE_TYPE_SPEC_TYPE already defined for ID %02x", @@ -594,9 +599,9 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } // Type chunks must be preceded by their TypeSpec chunks. - std::unique_ptr<TypeSpecBuilder>& builder_ptr = type_builder_map[type->id]; - if (builder_ptr != nullptr) { - builder_ptr->AddType(type.verified()); + auto& maybe_type_builder = type_builder_map[type->id]; + if (maybe_type_builder) { + maybe_type_builder->AddType(type.verified()); } else { LOG(ERROR) << StringPrintf( "RES_TABLE_TYPE_TYPE with ID %02x found without preceding RES_TABLE_TYPE_SPEC_TYPE.", diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 4c992becda7c..2c99f1aa3675 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -447,15 +447,19 @@ Res_png_9patch* Res_png_9patch::deserialize(void* inData) // -------------------------------------------------------------------- // -------------------------------------------------------------------- -ResStringPool::ResStringPool() - : mError(NO_INIT), mOwnedData(NULL), mHeader(NULL), mCache(NULL) -{ +ResStringPool::ResStringPool() : mError(NO_INIT), mOwnedData(NULL), mHeader(NULL), mCache(NULL) { } -ResStringPool::ResStringPool(const void* data, size_t size, bool copyData) - : mError(NO_INIT), mOwnedData(NULL), mHeader(NULL), mCache(NULL) -{ - setTo(data, size, copyData); +ResStringPool::ResStringPool(bool optimize_name_lookups) : ResStringPool() { + if (optimize_name_lookups) { + mIndexLookupCache.emplace(); + } +} + +ResStringPool::ResStringPool(const void* data, size_t size, bool copyData, + bool optimize_name_lookups) + : ResStringPool(optimize_name_lookups) { + setTo(data, size, copyData); } ResStringPool::~ResStringPool() @@ -683,6 +687,14 @@ status_t ResStringPool::setTo(incfs::map_ptr<void> data, size_t size, bool copyD mStylePoolSize = 0; } + if (mIndexLookupCache) { + if ((mHeader->flags & ResStringPool_header::UTF8_FLAG) != 0) { + mIndexLookupCache->first.reserve(mHeader->stringCount); + } else { + mIndexLookupCache->second.reserve(mHeader->stringCount); + } + } + return (mError=NO_ERROR); } @@ -708,6 +720,10 @@ void ResStringPool::uninit() free(mOwnedData); mOwnedData = NULL; } + if (mIndexLookupCache) { + mIndexLookupCache->first.clear(); + mIndexLookupCache->second.clear(); + } } /** @@ -824,11 +840,11 @@ base::expected<StringPiece16, NullOrIOError> ResStringPool::stringAt(size_t idx) // encLen must be less than 0x7FFF due to encoding. if ((uint32_t)(u8str+*u8len-strings) < mStringPoolSize) { - AutoMutex lock(mDecodeLock); + AutoMutex lock(mCachesLock); - if (mCache != NULL && mCache[idx] != NULL) { - return StringPiece16(mCache[idx], *u16len); - } + if (mCache != NULL && mCache[idx] != NULL) { + return StringPiece16(mCache[idx], *u16len); + } // Retrieve the actual length of the utf8 string if the // encoded length was truncated @@ -1093,12 +1109,24 @@ base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_ // block, start searching at the back. String8 str8(str, strLen); const size_t str8Len = str8.size(); + std::optional<AutoMutex> cacheLock; + if (mIndexLookupCache) { + cacheLock.emplace(mCachesLock); + if (auto it = mIndexLookupCache->first.find(std::string_view(str8)); + it != mIndexLookupCache->first.end()) { + return it->second; + } + } + for (int i=mHeader->stringCount-1; i>=0; i--) { const base::expected<StringPiece, NullOrIOError> s = string8At(i); if (UNLIKELY(IsIOError(s))) { return base::unexpected(s.error()); } if (s.has_value()) { + if (mIndexLookupCache) { + mIndexLookupCache->first.insert({*s, i}); + } if (kDebugStringPoolNoisy) { ALOGI("Looking at %s, i=%d\n", s->data(), i); } @@ -1151,20 +1179,32 @@ base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_ // most often this happens because we want to get IDs for style // span tags; since those always appear at the end of the string // block, start searching at the back. + std::optional<AutoMutex> cacheLock; + if (mIndexLookupCache) { + cacheLock.emplace(mCachesLock); + if (auto it = mIndexLookupCache->second.find({str, strLen}); + it != mIndexLookupCache->second.end()) { + return it->second; + } + } for (int i=mHeader->stringCount-1; i>=0; i--) { const base::expected<StringPiece16, NullOrIOError> s = stringAt(i); if (UNLIKELY(IsIOError(s))) { return base::unexpected(s.error()); } if (kDebugStringPoolNoisy) { - ALOGI("Looking at %s, i=%d\n", String8(s->data(), s->size()).c_str(), i); + ALOGI("Looking16 at %s, i=%d\n", String8(s->data(), s->size()).c_str(), i); } - if (s.has_value() && strLen == s->size() && - strzcmp16(s->data(), s->size(), str, strLen) == 0) { + if (s.has_value()) { + if (mIndexLookupCache) { + mIndexLookupCache->second.insert({*s, i}); + } + if (strLen == s->size() && strzcmp16(s->data(), s->size(), str, strLen) == 0) { if (kDebugStringPoolNoisy) { - ALOGI("MATCH!"); + ALOGI("MATCH16!"); } return i; + } } } } diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 60689128dffb..d9f7c2a1ac19 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -71,7 +71,7 @@ class OverlayDynamicRefTable : public DynamicRefTable { // Rewrites a compile-time overlay resource id to the runtime resource id of corresponding target // resource. - virtual status_t lookupResourceIdNoRewrite(uint32_t* resId) const; + status_t lookupResourceIdNoRewrite(uint32_t* resId) const; const Idmap_data_header* data_header_; const Idmap_overlay_entry* entries_; diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index 3a7287187781..413b27829474 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -99,6 +99,9 @@ enum : package_property_t { // The apk assets only contain the overlayable declarations information. PROPERTY_ONLY_OVERLAYABLES = 1U << 5U, + + // Optimize the resource lookups by name via an in-memory lookup table. + PROPERTY_OPTIMIZE_NAME_LOOKUPS = 1U << 6U, }; struct OverlayableInfo { @@ -285,7 +288,9 @@ class LoadedPackage { private: DISALLOW_COPY_AND_ASSIGN(LoadedPackage); - LoadedPackage() = default; + explicit LoadedPackage(bool optimize_name_lookups = false) + : type_string_pool_(optimize_name_lookups), key_string_pool_(optimize_name_lookups) { + } ResStringPool type_string_pool_; ResStringPool key_string_pool_; diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index c0514fdff469..3d1403d0e039 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -22,27 +22,28 @@ #include <android-base/expected.h> #include <android-base/unique_fd.h> - +#include <android/configuration.h> #include <androidfw/Asset.h> #include <androidfw/Errors.h> #include <androidfw/LocaleData.h> #include <androidfw/StringPiece.h> #include <utils/ByteOrder.h> #include <utils/Errors.h> +#include <utils/KeyedVector.h> #include <utils/String16.h> #include <utils/Vector.h> -#include <utils/KeyedVector.h> - #include <utils/threads.h> #include <stdint.h> #include <sys/types.h> -#include <android/configuration.h> - #include <array> #include <map> #include <memory> +#include <optional> +#include <string_view> +#include <unordered_map> +#include <utility> namespace android { @@ -512,23 +513,24 @@ struct ResStringPool_span class ResStringPool { public: - ResStringPool(); - ResStringPool(const void* data, size_t size, bool copyData=false); - virtual ~ResStringPool(); + ResStringPool(); + explicit ResStringPool(bool optimize_name_lookups); + ResStringPool(const void* data, size_t size, bool copyData = false, + bool optimize_name_lookups = false); + virtual ~ResStringPool(); - void setToEmpty(); - status_t setTo(incfs::map_ptr<void> data, size_t size, bool copyData=false); + void setToEmpty(); + status_t setTo(incfs::map_ptr<void> data, size_t size, bool copyData = false); - status_t getError() const; + status_t getError() const; - void uninit(); + void uninit(); - // Return string entry as UTF16; if the pool is UTF8, the string will - // be converted before returning. - inline base::expected<StringPiece16, NullOrIOError> stringAt( - const ResStringPool_ref& ref) const { - return stringAt(ref.index); - } + // Return string entry as UTF16; if the pool is UTF8, the string will + // be converted before returning. + inline base::expected<StringPiece16, NullOrIOError> stringAt(const ResStringPool_ref& ref) const { + return stringAt(ref.index); + } virtual base::expected<StringPiece16, NullOrIOError> stringAt(size_t idx) const; // Note: returns null if the string pool is not UTF8. @@ -557,7 +559,7 @@ private: void* mOwnedData; incfs::verified_map_ptr<ResStringPool_header> mHeader; size_t mSize; - mutable Mutex mDecodeLock; + mutable Mutex mCachesLock; incfs::map_ptr<uint32_t> mEntries; incfs::map_ptr<uint32_t> mEntryStyles; incfs::map_ptr<void> mStrings; @@ -566,6 +568,10 @@ private: incfs::map_ptr<uint32_t> mStyles; uint32_t mStylePoolSize; // number of uint32_t + mutable std::optional<std::pair<std::unordered_map<std::string_view, int>, + std::unordered_map<std::u16string_view, int>>> + mIndexLookupCache; + base::expected<StringPiece, NullOrIOError> stringDecodeAt( size_t idx, incfs::map_ptr<uint8_t> str, size_t encLen) const; }; @@ -1401,8 +1407,8 @@ struct ResTable_typeSpec // Must be 0. uint8_t res0; - // Must be 0. - uint16_t res1; + // Used to be reserved, if >0 specifies the number of ResTable_type entries for this spec. + uint16_t typesCount; // Number of uint32_t entry configuration masks that follow. uint32_t entryCount; diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index eebf8aabd89c..b40b73c111d0 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -716,7 +716,6 @@ cc_test { ], shared_libs: [ "libmemunreachable", - "server_configurable_flags", ], srcs: [ "tests/unit/main.cpp", diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index d4af0872e31e..a5a841e07d7a 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -23,6 +23,7 @@ #include <mutex> +#include "Properties.h" #include "utils/Macros.h" namespace android { @@ -60,7 +61,13 @@ public: static void setWideColorDataspace(ADataSpace dataspace); static void setSupportFp16ForHdr(bool supportFp16ForHdr); - static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; }; + static bool isSupportFp16ForHdr() { + if (!Properties::hdr10bitPlus) { + return false; + } + + return get()->mSupportFp16ForHdr; + }; static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index 00d049cde925..6ebfc63bdf26 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -41,6 +41,14 @@ inline bool deprecate_ui_fonts() { #endif // __ANDROID__ } +inline bool inter_character_justification() { +#ifdef __ANDROID__ + return com_android_text_flags_inter_character_justification(); +#else + return true; +#endif // __ANDROID__ +} + } // namespace text_feature } // namespace android diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index 16de21def0e3..71f7926930fc 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -379,7 +379,7 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { case kAlpha_8_SkColorType: formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support(); formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8_UNORM; - formatInfo.format = GL_R8; + formatInfo.format = GL_RED; formatInfo.type = GL_UNSIGNED_BYTE; formatInfo.vkFormat = VK_FORMAT_R8_UNORM; break; diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index d58c872dbc56..755332ff66fd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -38,6 +38,9 @@ namespace hwui_flags { constexpr bool clip_surfaceviews() { return false; } +constexpr bool hdr_10bit_plus() { + return false; +} } // namespace hwui_flags #endif @@ -105,6 +108,7 @@ bool Properties::isSystemOrPersistent = false; float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number bool Properties::clipSurfaceViews = false; +bool Properties::hdr10bitPlus = false; StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; @@ -177,6 +181,7 @@ bool Properties::load() { clipSurfaceViews = base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews()); + hdr10bitPlus = hwui_flags::hdr_10bit_plus(); return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index b956facf6f90..ec53070f6cb8 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -336,6 +336,7 @@ public: static float maxHdrHeadroomOn8bit; static bool clipSurfaceViews; + static bool hdr10bitPlus; static StretchEffectBehavior getStretchEffectBehavior() { return stretchEffectBehavior; diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index f4ee36ec66d1..bbb142014ed8 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -131,11 +131,6 @@ std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( const std::vector<minikin::FontVariation>& variations) const { SkFontArguments args; - int ttcIndex; - std::unique_ptr<SkStreamAsset> stream(mTypeface->openStream(&ttcIndex)); - LOG_ALWAYS_FATAL_IF(stream == nullptr, "openStream failed"); - - args.setCollectionIndex(ttcIndex); std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation; skVariation.resize(variations.size()); for (size_t i = 0; i < variations.size(); i++) { @@ -143,11 +138,10 @@ std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( skVariation[i].value = SkFloatToScalar(variations[i].value); } args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); - sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); - sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args)); + sk_sp<SkTypeface> face = mTypeface->makeClone(args); return std::make_shared<MinikinFontSkia>(std::move(face), mSourceId, mFontData, mFontSize, - mFilePath, ttcIndex, variations); + mFilePath, mTtcIndex, variations); } // hinting<<16 | edging<<8 | bools:5bits diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 833069f363c8..56133699d5f5 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -72,10 +72,13 @@ minikin::Layout MinikinUtils::doLayout(const Paint* paint, minikin::Bidi bidiFla const minikin::Range contextRange(contextStart, contextStart + contextCount); const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit(); const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit(); + const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification() + ? paint->getRunFlag() + : minikin::RunFlag::NONE; if (mt == nullptr) { return minikin::Layout(textBuf.substr(contextRange), range - contextStart, bidiFlags, - minikinPaint, startHyphen, endHyphen); + minikinPaint, startHyphen, endHyphen, minikinRunFlag); } else { return mt->buildLayout(textBuf, range, contextRange, minikinPaint, startHyphen, endHyphen); } @@ -102,9 +105,12 @@ float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags, const minikin::Range range(start, start + count); const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit(); const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit(); + const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification() + ? paint->getRunFlag() + : minikin::RunFlag::NONE; return minikin::Layout::measureText(textBuf, range, bidiFlags, minikinPaint, startHyphen, - endHyphen, advances, bounds, clusterCount); + endHyphen, advances, bounds, clusterCount, minikinRunFlag); } minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::Bidi bidiFlags, diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h index ef4dce57bf46..708f96e5a070 100644 --- a/libs/hwui/hwui/Paint.h +++ b/libs/hwui/hwui/Paint.h @@ -25,6 +25,7 @@ #include <minikin/FontFamily.h> #include <minikin/FontFeature.h> #include <minikin/Hyphenator.h> +#include <minikin/Layout.h> #include <string> @@ -144,6 +145,9 @@ public: bool isDevKern() const { return mDevKern; } void setDevKern(bool d) { mDevKern = d; } + minikin::RunFlag getRunFlag() const { return mRunFlag; } + void setRunFlag(minikin::RunFlag runFlag) { mRunFlag = runFlag; } + // Deprecated -- bitmapshaders will be taking this flag explicitly bool isFilterBitmap() const { return mFilterBitmap; } void setFilterBitmap(bool filter) { mFilterBitmap = filter; } @@ -188,6 +192,7 @@ private: bool mStrikeThru = false; bool mUnderline = false; bool mDevKern = false; + minikin::RunFlag mRunFlag = minikin::RunFlag::NONE; }; } // namespace android diff --git a/libs/hwui/hwui/PaintImpl.cpp b/libs/hwui/hwui/PaintImpl.cpp index aac928f85924..c32ea01db7b7 100644 --- a/libs/hwui/hwui/PaintImpl.cpp +++ b/libs/hwui/hwui/PaintImpl.cpp @@ -47,8 +47,8 @@ Paint::Paint(const Paint& paint) , mFilterBitmap(paint.mFilterBitmap) , mStrikeThru(paint.mStrikeThru) , mUnderline(paint.mUnderline) - , mDevKern(paint.mDevKern) {} - + , mDevKern(paint.mDevKern) + , mRunFlag(paint.mRunFlag) {} Paint::~Paint() {} @@ -68,21 +68,19 @@ Paint& Paint::operator=(const Paint& other) { mStrikeThru = other.mStrikeThru; mUnderline = other.mUnderline; mDevKern = other.mDevKern; + mRunFlag = other.mRunFlag; return *this; } bool operator==(const Paint& a, const Paint& b) { - return static_cast<const SkPaint&>(a) == static_cast<const SkPaint&>(b) && - a.mFont == b.mFont && - a.mLooper == b.mLooper && - a.mLetterSpacing == b.mLetterSpacing && a.mWordSpacing == b.mWordSpacing && - a.mFontFeatureSettings == b.mFontFeatureSettings && + return static_cast<const SkPaint&>(a) == static_cast<const SkPaint&>(b) && a.mFont == b.mFont && + a.mLooper == b.mLooper && a.mLetterSpacing == b.mLetterSpacing && + a.mWordSpacing == b.mWordSpacing && a.mFontFeatureSettings == b.mFontFeatureSettings && a.mMinikinLocaleListId == b.mMinikinLocaleListId && a.mFamilyVariant == b.mFamilyVariant && a.mHyphenEdit == b.mHyphenEdit && a.mTypeface == b.mTypeface && a.mAlign == b.mAlign && - a.mFilterBitmap == b.mFilterBitmap && - a.mStrikeThru == b.mStrikeThru && a.mUnderline == b.mUnderline && - a.mDevKern == b.mDevKern; + a.mFilterBitmap == b.mFilterBitmap && a.mStrikeThru == b.mStrikeThru && + a.mUnderline == b.mUnderline && a.mDevKern == b.mDevKern && a.mRunFlag == b.mRunFlag; } void Paint::reset() { @@ -96,6 +94,7 @@ void Paint::reset() { mStrikeThru = false; mUnderline = false; mDevKern = false; + mRunFlag = minikin::RunFlag::NONE; } void Paint::setLooper(sk_sp<BlurDrawLooper> looper) { @@ -133,6 +132,8 @@ static const uint32_t sForceAutoHinting = 0x800; // flags related to minikin::Paint static const uint32_t sUnderlineFlag = 0x08; static const uint32_t sStrikeThruFlag = 0x10; +static const uint32_t sTextRunLeftEdge = 0x2000; +static const uint32_t sTextRunRightEdge = 0x4000; // flags no longer supported on native side (but mirrored for compatibility) static const uint32_t sDevKernFlag = 0x100; @@ -186,6 +187,12 @@ uint32_t Paint::getJavaFlags() const { flags |= -(int)mUnderline & sUnderlineFlag; flags |= -(int)mDevKern & sDevKernFlag; flags |= -(int)mFilterBitmap & sFilterBitmapFlag; + if (mRunFlag & minikin::RunFlag::LEFT_EDGE) { + flags |= sTextRunLeftEdge; + } + if (mRunFlag & minikin::RunFlag::RIGHT_EDGE) { + flags |= sTextRunRightEdge; + } return flags; } @@ -196,6 +203,15 @@ void Paint::setJavaFlags(uint32_t flags) { mUnderline = (flags & sUnderlineFlag) != 0; mDevKern = (flags & sDevKernFlag) != 0; mFilterBitmap = (flags & sFilterBitmapFlag) != 0; + + std::underlying_type<minikin::RunFlag>::type rawFlag = minikin::RunFlag::NONE; + if (flags & sTextRunLeftEdge) { + rawFlag |= minikin::RunFlag::LEFT_EDGE; + } + if (flags & sTextRunRightEdge) { + rawFlag |= minikin::RunFlag::RIGHT_EDGE; + } + mRunFlag = static_cast<minikin::RunFlag>(rawFlag); } } // namespace android diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index 8ba750372d18..d5725935551a 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -613,6 +613,12 @@ static void drawTextChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray c Paint* paint = reinterpret_cast<Paint*>(paintHandle); const Typeface* typeface = paint->getAndroidTypeface(); ScopedCharArrayRO text(env, charArray); + + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + // drawTextString and drawTextChars doesn't use context info get_canvas(canvasHandle)->drawText( text.get() + index, count, // text buffer @@ -620,6 +626,7 @@ static void drawTextChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray c 0, count, // context range x, y, // draw position static_cast<minikin::Bidi>(bidiFlags), *paint, typeface, nullptr /* measured text */); + paint->setRunFlag(originalRunFlag); } static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring strObj, @@ -629,6 +636,12 @@ static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring str Paint* paint = reinterpret_cast<Paint*>(paintHandle); const Typeface* typeface = paint->getAndroidTypeface(); const int count = end - start; + + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + // drawTextString and drawTextChars doesn't use context info get_canvas(canvasHandle)->drawText( text.get() + start, count, // text buffer @@ -636,6 +649,7 @@ static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring str 0, count, // context range x, y, // draw position static_cast<minikin::Bidi>(bidiFlags), *paint, typeface, nullptr /* measured text */); + paint->setRunFlag(originalRunFlag); } static void drawTextRunChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray charArray, @@ -681,9 +695,15 @@ static void drawTextOnPathChars(JNIEnv* env, jobject, jlong canvasHandle, jcharA jchar* jchars = env->GetCharArrayElements(text, NULL); + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + get_canvas(canvasHandle)->drawTextOnPath(jchars + index, count, static_cast<minikin::Bidi>(bidiFlags), *path, hOffset, vOffset, *paint, typeface); + paint->setRunFlag(originalRunFlag); env->ReleaseCharArrayElements(text, jchars, 0); } @@ -697,9 +717,15 @@ static void drawTextOnPathString(JNIEnv* env, jobject, jlong canvasHandle, jstri const jchar* jchars = env->GetStringChars(text, NULL); int count = env->GetStringLength(text); + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + get_canvas(canvasHandle)->drawTextOnPath(jchars, count, static_cast<minikin::Bidi>(bidiFlags), *path, hOffset, vOffset, *paint, typeface); + paint->setRunFlag(originalRunFlag); env->ReleaseStringChars(text, jchars); } diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index c5ffbb70213e..c8d598702a7c 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -118,7 +118,7 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, - const HardwareBufferRenderParams& bufferParams) { + const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) { if (!isCapturingSkp() && !mHardwareBuffer) { mEglManager.damageFrame(frame, dirty); } @@ -171,6 +171,7 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( // Draw visual debugging features if (CC_UNLIKELY(Properties::showDirtyRegions || ProfileType::None != Properties::getProfileType())) { + std::scoped_lock lock(profilerLock); SkCanvas* profileCanvas = surface->getCanvas(); SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h index 098a74655831..ebe8b6e15d44 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h @@ -42,7 +42,8 @@ public: const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler, - const renderthread::HardwareBufferRenderParams& bufferParams) override; + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override; GrSurfaceOrigin getSurfaceOrigin() override { return kBottomLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, const SkRect& screenDirty, FrameInfo* currentFrameInfo, diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index e0f1f6ef44be..326b6ed77fe0 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -650,9 +650,14 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); break; case ColorMode::Hdr: - mSurfaceColorType = SkColorType::kN32_SkColorType; - mSurfaceColorSpace = SkColorSpace::MakeRGB( - GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + if (DeviceInfo::get()->isSupportFp16ForHdr()) { + mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + } else { + mSurfaceColorType = SkColorType::kN32_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } break; case ColorMode::Hdr10: mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType; @@ -669,8 +674,13 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { void SkiaPipeline::setTargetSdrHdrRatio(float ratio) { if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) { mTargetSdrHdrRatio = ratio; - mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio), - SkNamedGamut::kDisplayP3); + + if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr()) { + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + } else { + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } } else { mTargetSdrHdrRatio = 1.f; } diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index d74748936d15..fd0a8e06f39c 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -75,7 +75,7 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, - const HardwareBufferRenderParams& bufferParams) { + const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) { sk_sp<SkSurface> backBuffer; SkMatrix preTransform; if (mHardwareBuffer) { @@ -103,6 +103,7 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( // Draw visual debugging features if (CC_UNLIKELY(Properties::showDirtyRegions || ProfileType::None != Properties::getProfileType())) { + std::scoped_lock lock(profilerLock); SkCanvas* profileCanvas = backBuffer->getCanvas(); SkAutoCanvasRestore saver(profileCanvas, true); profileCanvas->concat(preTransform); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h index e2ea57d32cd5..624eaa51a584 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h @@ -42,7 +42,8 @@ public: const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode> >& renderNodes, FrameInfoVisualizer* profiler, - const renderthread::HardwareBufferRenderParams& bufferParams) override; + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override; GrSurfaceOrigin getSurfaceOrigin() override { return kTopLeft_GrSurfaceOrigin; } bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, const SkRect& screenDirty, FrameInfo* currentFrameInfo, diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index eb4d4948dc53..ac2a9366a1f6 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -124,24 +124,19 @@ void CacheManager::trimMemory(TrimLevel mode) { // flush and submit all work to the gpu and wait for it to finish mGrContext->flushAndSubmit(GrSyncCpu::kYes); - switch (mode) { - case TrimLevel::BACKGROUND: - mGrContext->freeGpuResources(); - SkGraphics::PurgeAllCaches(); - mRenderThread.destroyRenderingContext(); - break; - case TrimLevel::UI_HIDDEN: - // Here we purge all the unlocked scratch resources and then toggle the resources cache - // limits between the background and max amounts. This causes the unlocked resources - // that have persistent data to be purged in LRU order. - mGrContext->setResourceCacheLimit(mBackgroundResourceBytes); - SkGraphics::SetFontCacheLimit(mBackgroundCpuFontCacheBytes); - mGrContext->purgeUnlockedResources(toSkiaEnum(mMemoryPolicy.purgeScratchOnly)); - mGrContext->setResourceCacheLimit(mMaxResourceBytes); - SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); - break; - default: - break; + if (mode >= TrimLevel::BACKGROUND) { + mGrContext->freeGpuResources(); + SkGraphics::PurgeAllCaches(); + mRenderThread.destroyRenderingContext(); + } else if (mode == TrimLevel::UI_HIDDEN) { + // Here we purge all the unlocked scratch resources and then toggle the resources cache + // limits between the background and max amounts. This causes the unlocked resources + // that have persistent data to be purged in LRU order. + mGrContext->setResourceCacheLimit(mBackgroundResourceBytes); + SkGraphics::SetFontCacheLimit(mBackgroundCpuFontCacheBytes); + mGrContext->purgeUnlockedResources(toSkiaEnum(mMemoryPolicy.purgeScratchOnly)); + mGrContext->setResourceCacheLimit(mMaxResourceBytes); + SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); } } diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 56b52dc2abe7..9c7f7cc24266 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -625,14 +625,9 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { { // FrameInfoVisualizer accesses the frame events, which cannot be mutated mid-draw // or it can lead to memory corruption. - // This lock is overly broad, but it's the quickest fix since this mutex is otherwise - // not visible to IRenderPipeline much less FrameInfoVisualizer. And since this is - // the thread we're primarily concerned about being responsive, this being too broad - // shouldn't pose a performance issue. - std::scoped_lock lock(mFrameMetricsReporterMutex); - drawResult = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, - &mLayerUpdateQueue, mContentDrawBounds, mOpaque, - mLightInfo, mRenderNodes, &(profiler()), mBufferParams); + drawResult = mRenderPipeline->draw( + frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue, mContentDrawBounds, + mOpaque, mLightInfo, mRenderNodes, &(profiler()), mBufferParams, profilerLock()); } uint64_t frameCompleteNr = getFrameNumber(); @@ -762,7 +757,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { mCurrentFrameInfo->markFrameCompleted(); mCurrentFrameInfo->set(FrameInfoIndex::GpuCompleted) = mCurrentFrameInfo->get(FrameInfoIndex::FrameCompleted); - std::scoped_lock lock(mFrameMetricsReporterMutex); + std::scoped_lock lock(mFrameInfoMutex); mJankTracker.finishFrame(*mCurrentFrameInfo, mFrameMetricsReporter, frameCompleteNr, mSurfaceControlGenerationId); } @@ -791,7 +786,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { void CanvasContext::reportMetricsWithPresentTime() { { // acquire lock - std::scoped_lock lock(mFrameMetricsReporterMutex); + std::scoped_lock lock(mFrameInfoMutex); if (mFrameMetricsReporter == nullptr) { return; } @@ -826,7 +821,7 @@ void CanvasContext::reportMetricsWithPresentTime() { forthBehind->set(FrameInfoIndex::DisplayPresentTime) = presentTime; { // acquire lock - std::scoped_lock lock(mFrameMetricsReporterMutex); + std::scoped_lock lock(mFrameInfoMutex); if (mFrameMetricsReporter != nullptr) { mFrameMetricsReporter->reportFrameMetrics(forthBehind->data(), true /*hasPresentTime*/, frameNumber, surfaceControlId); @@ -835,7 +830,7 @@ void CanvasContext::reportMetricsWithPresentTime() { } void CanvasContext::addFrameMetricsObserver(FrameMetricsObserver* observer) { - std::scoped_lock lock(mFrameMetricsReporterMutex); + std::scoped_lock lock(mFrameInfoMutex); if (mFrameMetricsReporter.get() == nullptr) { mFrameMetricsReporter.reset(new FrameMetricsReporter()); } @@ -849,7 +844,7 @@ void CanvasContext::addFrameMetricsObserver(FrameMetricsObserver* observer) { } void CanvasContext::removeFrameMetricsObserver(FrameMetricsObserver* observer) { - std::scoped_lock lock(mFrameMetricsReporterMutex); + std::scoped_lock lock(mFrameInfoMutex); if (mFrameMetricsReporter.get() != nullptr) { mFrameMetricsReporter->removeObserver(observer); if (!mFrameMetricsReporter->hasObservers()) { @@ -886,7 +881,7 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber, surfaceControlId); if (frameInfo != nullptr) { - std::scoped_lock lock(instance->mFrameMetricsReporterMutex); + std::scoped_lock lock(instance->mFrameInfoMutex); frameInfo->set(FrameInfoIndex::FrameCompleted) = std::max(gpuCompleteTime, frameInfo->get(FrameInfoIndex::SwapBuffersCompleted)); frameInfo->set(FrameInfoIndex::GpuCompleted) = std::max( diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index be9b649030ae..e2e3fa35b9b0 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -164,6 +164,7 @@ public: void notifyFramePending(); FrameInfoVisualizer& profiler() { return mProfiler; } + std::mutex& profilerLock() { return mFrameInfoMutex; } void dumpFrames(int fd); void resetFrameStats(); @@ -342,9 +343,8 @@ private: std::string mName; JankTracker mJankTracker; FrameInfoVisualizer mProfiler; - std::unique_ptr<FrameMetricsReporter> mFrameMetricsReporter - GUARDED_BY(mFrameMetricsReporterMutex); - std::mutex mFrameMetricsReporterMutex; + std::unique_ptr<FrameMetricsReporter> mFrameMetricsReporter GUARDED_BY(mFrameInfoMutex); + std::mutex mFrameInfoMutex; std::set<RenderNode*> mPrefetchedLayers; diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index facf30b83b07..2904dfe76f40 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -441,22 +441,32 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, colorMode = ColorMode::Default; } - if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) { + // TODO: maybe we want to get rid of the WCG check if overlay properties just works? + const bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() || + DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType; + + if (canUseFp16) { if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { colorMode = ColorMode::Default; } else { config = mEglConfigF16; } } + if (EglExtensions.glColorSpace) { attribs[0] = EGL_GL_COLORSPACE_KHR; switch (colorMode) { case ColorMode::Default: attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; break; + case ColorMode::Hdr: + if (canUseFp16) { + attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; + break; + // No fp16 support so fallthrough to HDR10 + } // We don't have an EGL colorspace for extended range P3 that's used for HDR // So override it after configuring the EGL context - case ColorMode::Hdr: case ColorMode::Hdr10: overrideWindowDataSpaceForHdr = true; attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index 9c879d5a58d1..b8c3a4de2bd4 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -68,7 +68,8 @@ public: const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, - const HardwareBufferRenderParams& bufferParams) = 0; + const HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) = 0; virtual bool swapBuffers(const Frame& frame, IRenderPipeline::DrawResult&, const SkRect& screenDirty, FrameInfo* currentFrameInfo, bool* requireSwap) = 0; diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index b002bbf20c08..ea136edf46be 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -1079,7 +1079,7 @@ public final class AudioFormat implements Parcelable { * @return one of the values that can be set in {@link Builder#setEncoding(int)} or * {@link AudioFormat#ENCODING_INVALID} if not set. */ - public int getEncoding() { + public @Encoding int getEncoding() { return mEncoding; } diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 46db77708521..587e35b4b1fc 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -16,6 +16,9 @@ package android.media; +import static com.android.media.codec.flags.Flags.FLAG_CODEC_IMPORTANCE; + +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1635,6 +1638,34 @@ public final class MediaFormat { */ public static final String KEY_ALLOW_FRAME_DROP = "allow-frame-drop"; + /** + * A key describing the desired codec importance for the application. + * <p> + * The associated value is a positive integer including zero. + * Higher value means lesser importance. + * <p> + * The resource manager may use the codec importance, along with other factors + * when reclaiming codecs from an application. + * The specifics of reclaim policy is device dependent, but specifying the codec importance, + * will allow the resource manager to prioritize reclaiming less important codecs + * (assigned higher values) from the (reclaim) requesting application first. + * So, the codec importance is only relevant within the context of that application. + * <p> + * The codec importance can be set: + * <ul> + * <li>through {@link MediaCodec#configure}. </li> + * <li>through {@link MediaCodec#setParameters} if the codec has been configured already, + * which allows the users to change the codec importance multiple times. + * </ul> + * Any change/update in codec importance is guaranteed upon the completion of the function call + * that sets the codec importance. So, in case of concurrent codec operations, + * make sure to wait for the change in codec importance, before using another codec. + * Note that unless specified, by default the codecs will have highest importance (of value 0). + * + */ + @FlaggedApi(FLAG_CODEC_IMPORTANCE) + public static final String KEY_IMPORTANCE = "importance"; + /* package private */ MediaFormat(@NonNull Map<String, Object> map) { mMap = map; } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 9616b5d44540..62412df97043 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -734,7 +734,7 @@ public final class MediaRouter2 { * request. * @param transferInitiatorPackageName the package name of the app that initiated the transfer. * This value is used with the user handle to populate {@link - * RoutingController#wasTransferRequestedBySelf()}. + * RoutingController#wasTransferInitiatedBySelf()}. * @hide */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) @@ -1550,11 +1550,11 @@ public final class MediaRouter2 { } /** - * Returns whether the transfer was requested by the calling app (as determined by comparing + * Returns whether the transfer was initiated by the calling app (as determined by comparing * {@link UserHandle} and package name). */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) - public boolean wasTransferRequestedBySelf() { + public boolean wasTransferInitiatedBySelf() { RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 7f9588644052..df9ecdc98e85 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -83,3 +83,10 @@ flag { description: "Notify ActivityManager with the changes in playback state of the media session." bug: "295518668" } + +flag { + name: "enable_prevention_of_keep_alive_route_providers" + namespace: "media_solutions" + description: "Enables mechanisms to prevent route providers from keeping malicious apps alive." + bug: "263520343" +} diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 8ce1b6d5264d..3d927d36a369 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -121,7 +121,7 @@ interface IMediaProjectionManager { @EnforcePermission("MANAGE_MEDIA_PROJECTION") @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - void addCallback(IMediaProjectionWatcherCallback callback); + MediaProjectionInfo addCallback(IMediaProjectionWatcherCallback callback); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java index ff608565d2e5..c82039297d6e 100644 --- a/media/java/android/media/projection/MediaProjectionInfo.java +++ b/media/java/android/media/projection/MediaProjectionInfo.java @@ -16,6 +16,7 @@ package android.media.projection; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; @@ -26,15 +27,18 @@ import java.util.Objects; public final class MediaProjectionInfo implements Parcelable { private final String mPackageName; private final UserHandle mUserHandle; + private final IBinder mLaunchCookie; - public MediaProjectionInfo(String packageName, UserHandle handle) { + public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) { mPackageName = packageName; mUserHandle = handle; + mLaunchCookie = launchCookie; } public MediaProjectionInfo(Parcel in) { mPackageName = in.readString(); mUserHandle = UserHandle.readFromParcel(in); + mLaunchCookie = in.readStrongBinder(); } public String getPackageName() { @@ -45,12 +49,16 @@ public final class MediaProjectionInfo implements Parcelable { return mUserHandle; } + public IBinder getLaunchCookie() { + return mLaunchCookie; + } + @Override public boolean equals(Object o) { - if (o instanceof MediaProjectionInfo) { - final MediaProjectionInfo other = (MediaProjectionInfo) o; + if (o instanceof MediaProjectionInfo other) { return Objects.equals(other.mPackageName, mPackageName) - && Objects.equals(other.mUserHandle, mUserHandle); + && Objects.equals(other.mUserHandle, mUserHandle) + && Objects.equals(other.mLaunchCookie, mLaunchCookie); } return false; } @@ -64,7 +72,8 @@ public final class MediaProjectionInfo implements Parcelable { public String toString() { return "MediaProjectionInfo{mPackageName=" + mPackageName + ", mUserHandle=" - + mUserHandle + "}"; + + mUserHandle + ", mLaunchCookie" + + mLaunchCookie + "}"; } @Override @@ -76,6 +85,7 @@ public final class MediaProjectionInfo implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeString(mPackageName); UserHandle.writeToParcel(mUserHandle, out); + out.writeStrongBinder(mLaunchCookie); } public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR = diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl index a8ffd2b4dd8f..2f6575e65077 100644 --- a/media/java/android/media/tv/ITvInputManager.aidl +++ b/media/java/android/media/tv/ITvInputManager.aidl @@ -149,4 +149,7 @@ interface ITvInputManager { // For CTS purpose only. Add/remove a TvInputHardware device void addHardwareDevice(in int deviceId); void removeHardwareDevice(in int deviceId); + + // For freezing video playback + void setVideoFrozen(in IBinder sessionToken, boolean isFrozen, int userId); } diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl index e37ee6e342e5..a93f18db3dca 100644 --- a/media/java/android/media/tv/ITvInputSession.aidl +++ b/media/java/android/media/tv/ITvInputSession.aidl @@ -83,4 +83,7 @@ oneway interface ITvInputSession { // For TV messages void notifyTvMessage(int type, in Bundle data); void setTvMessageEnabled(int type, boolean enabled); + + // For freezing video + void setVideoFrozen(boolean isFrozen); } diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java index ae3ee6535c71..921104dc70c8 100644 --- a/media/java/android/media/tv/ITvInputSessionWrapper.java +++ b/media/java/android/media/tv/ITvInputSessionWrapper.java @@ -81,6 +81,7 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand private static final int DO_NOTIFY_TV_MESSAGE = 32; private static final int DO_STOP_PLAYBACK = 33; private static final int DO_START_PLAYBACK = 34; + private static final int DO_SET_VIDEO_FROZEN = 35; private final boolean mIsRecordingSession; private final HandlerCaller mCaller; @@ -296,6 +297,10 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mTvInputSessionImpl.startPlayback(); break; } + case DO_SET_VIDEO_FROZEN: { + mTvInputSessionImpl.setVideoFrozen((Boolean) msg.obj); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -483,6 +488,11 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand } @Override + public void setVideoFrozen(boolean isFrozen) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VIDEO_FROZEN, isFrozen)); + } + + @Override public void notifyTvMessage(int type, Bundle data) { mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_NOTIFY_TV_MESSAGE, type, data)); } diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index db0195056074..7f8f1a3f22ef 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -16,6 +16,7 @@ package android.media.tv; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -29,6 +30,7 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; +import android.media.tv.flags.Flags; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; @@ -2540,9 +2542,9 @@ public final class TvContract { * <p>This is used to indicate the broadcast visibility type defined in the underlying * broadcast standard or country/operator profile, if applicable. For example, * {@code visible_service_flag} and {@code numeric_selection_flag} of - * {@code service_attribute_descriptor} in D-Book, {@code visible_service_flag} and - * {@code selectable_service_flag} of {@code ciplus_service_descriptor} in CI Plus 1.3 - * specification. + * {@code service_attribute_descriptor} in D-Book, the specification for UK-based TV + * products, {@code visible_service_flag} and {@code selectable_service_flag} of + * {@code ciplus_service_descriptor} in the CI Plus 1.3 specification. * * <p>The value should match one of the following: * {@link #BROADCAST_VISIBILITY_TYPE_VISIBLE}, @@ -2553,8 +2555,8 @@ public final class TvContract { * by default. * * <p>Type: INTEGER - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type"; /** @hide */ @@ -2571,8 +2573,8 @@ public final class TvContract { * visible from users and selectable by users via normal service navigation mechanisms. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; /** @@ -2581,18 +2583,18 @@ public final class TvContract { * the logical channel number. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; /** * The broadcast visibility type for invisible services. Use this type when the service - * is invisible from users and unselectable by users via any of normal service navigation - * mechanisms. + * is invisible from users and not able to be selected by users via any of the normal + * service navigation mechanisms. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; private Channels() {} diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index c685a5adb08b..51b2542688a6 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -3334,6 +3334,18 @@ public final class TvInputManager { } } + void setVideoFrozen(boolean isFrozen) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.setVideoFrozen(mToken, isFrozen, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Sends TV messages to the service for testing purposes */ diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 55fa51755177..76d8e50f94be 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -1558,6 +1558,17 @@ public abstract class TvInputService extends Service { } /** + * Called when a request to freeze the video is received from the TV app. The audio should + * continue playback while the video is frozen. + * + * <p> This should freeze the video to the last frame when the state is set to {@code true}. + * @param isFrozen whether or not the video should be frozen. + * @hide + */ + public void onSetVideoFrozen(boolean isFrozen) { + } + + /** * Called when the application requests to play a given recorded TV program. * * @param recordedProgramUri The URI of a recorded TV program. @@ -2034,6 +2045,13 @@ public abstract class TvInputService extends Service { } /** + * Calls {@link #onSetVideoFrozen(boolean)}. + */ + void setVideoFrozen(boolean isFrozen) { + onSetVideoFrozen(isFrozen); + } + + /** * Calls {@link #onTimeShiftPlay(Uri)}. */ void timeShiftPlay(Uri recordedProgramUri) { diff --git a/media/java/android/media/tv/TvTrackInfo.java b/media/java/android/media/tv/TvTrackInfo.java index 78d7d7631145..2ebb19a7767e 100644 --- a/media/java/android/media/tv/TvTrackInfo.java +++ b/media/java/android/media/tv/TvTrackInfo.java @@ -55,6 +55,27 @@ public final class TvTrackInfo implements Parcelable { */ public static final int TYPE_SUBTITLE = 2; + /** + * The component tag identifies a component carried by a MPEG-2 TS. + * + * This corresponds to the component_tag in the component descriptor in the + * Elementary Stream loop of the stream in the Program Map Table + * (PMT) [EN 300 468], or undefined if the component is not carried in an + * MPEG-2 TS. + * + * @hide + */ + public static final String EXTRA_BUNDLE_KEY_COMPONENT_TAG = "component_tag"; + + /** + * The MPEG Program ID (PID) of the component in the MPEG2-TS in + * which it is carried, or undefined if the component is not carried in an + * MPEG-2 TS. + * + * @hide + */ + public static final String EXTRA_BUNDLE_KEY_PID = "pid"; + private final int mType; private final String mId; private final String mLanguage; diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 233f96675543..cb45661a01ac 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -675,6 +675,23 @@ public class TvView extends ViewGroup { } /** + * Sets whether or not the video is frozen. While the video is frozen, audio playback will + * continue. + * + * <p> This should be invoked after a {@link TvInteractiveAppService.Session#requestCommand} is + * received with the command to freeze the video. + * + * <p> This will freeze the video to the last frame when the state is set to {@code true}. + * @param isFrozen whether or not the video is frozen. + * @hide + */ + public void setVideoFrozen(boolean isFrozen) { + if (mSession != null) { + mSession.setVideoFrozen(isFrozen); + } + } + + /** * Sends TV messages to the session for testing purposes * * @hide diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl new file mode 100644 index 000000000000..34d96b374a35 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdClient.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.view.InputChannel; + +/** + * Interface a client of the ITvAdManager implements, to identify itself and receive + * information about changes to the state of each TV AD service. + * @hide + */ +oneway interface ITvAdClient { + void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq); + void onSessionReleased(int seq); + void onLayoutSurface(int left, int top, int right, int bottom, int seq); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl index 92cc923dc9ab..a747e4995869 100644 --- a/media/java/android/media/tv/ad/ITvAdManager.aidl +++ b/media/java/android/media/tv/ad/ITvAdManager.aidl @@ -16,10 +16,25 @@ package android.media.tv.ad; +import android.media.tv.ad.ITvAdClient; +import android.media.tv.ad.ITvAdManagerCallback; +import android.media.tv.ad.TvAdServiceInfo; +import android.view.Surface; + /** * Interface to the TV AD service. * @hide */ interface ITvAdManager { + List<TvAdServiceInfo> getTvAdServiceList(int userId); + void createSession( + in ITvAdClient client, in String serviceId, in String type, int seq, int userId); + void releaseSession(in IBinder sessionToken, int userId); void startAdService(in IBinder sessionToken, int userId); + void setSurface(in IBinder sessionToken, in Surface surface, int userId); + void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height, + int userId); + + void registerCallback(in ITvAdManagerCallback callback, int userId); + void unregisterCallback(in ITvAdManagerCallback callback, int userId); } diff --git a/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl new file mode 100644 index 000000000000..f55f67e616db --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +/** + * Interface to receive callbacks from ITvAdManager regardless of sessions. + * @hide + */ +oneway interface ITvAdManagerCallback { + void onAdServiceAdded(in String serviceId); + void onAdServiceRemoved(in String serviceId); + void onAdServiceUpdated(in String serviceId); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdService.aidl b/media/java/android/media/tv/ad/ITvAdService.aidl new file mode 100644 index 000000000000..3bb04097abca --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdService.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.media.tv.ad.ITvAdServiceCallback; +import android.media.tv.ad.ITvAdSessionCallback; +import android.os.Bundle; +import android.view.InputChannel; + +/** + * Top-level interface to a TV AD component (implemented in a Service). It's used for + * TvAdManagerService to communicate with TvAdService. + * @hide + */ +oneway interface ITvAdService { + void registerCallback(in ITvAdServiceCallback callback); + void unregisterCallback(in ITvAdServiceCallback callback); + void createSession(in InputChannel channel, in ITvAdSessionCallback callback, + in String serviceId, in String type); + void sendAppLinkCommand(in Bundle command); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl new file mode 100644 index 000000000000..a087181e097d --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +/** + * Helper interface for ITvAdService to allow the TvAdService to notify the TvAdManagerService. + * @hide + */ +oneway interface ITvAdServiceCallback { +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl index b834f1b9fb92..751257ce4d4e 100644 --- a/media/java/android/media/tv/ad/ITvAdSession.aidl +++ b/media/java/android/media/tv/ad/ITvAdSession.aidl @@ -16,10 +16,15 @@ package android.media.tv.ad; +import android.view.Surface; + /** - * Sub-interface of ITvAdService which is created per session and has its own context. + * Sub-interface of ITvAdService.aidl which is created per session and has its own context. * @hide */ oneway interface ITvAdSession { + void release(); void startAdService(); + void setSurface(in Surface surface); + void dispatchSurfaceChanged(int format, int width, int height); } diff --git a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl new file mode 100644 index 000000000000..f21ef198ab01 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.media.tv.ad.ITvAdSession; + +/** + * Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is + * a related event. + * @hide + */ +oneway interface ITvAdSessionCallback { + void onSessionCreated(in ITvAdSession session); + void onLayoutSurface(int left, int top, int right, int bottom); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java new file mode 100644 index 000000000000..4df2783f6511 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.ad; + +import android.content.Context; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +/** + * Implements the internal ITvAdSession interface. + * @hide + */ +public class ITvAdSessionWrapper + extends ITvAdSession.Stub implements HandlerCaller.Callback { + + private static final String TAG = "ITvAdSessionWrapper"; + + private static final int EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS = 1000; + private static final int EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS = 5 * 1000; + private static final int DO_RELEASE = 1; + private static final int DO_SET_SURFACE = 2; + private static final int DO_DISPATCH_SURFACE_CHANGED = 3; + + private final HandlerCaller mCaller; + private TvAdService.Session mSessionImpl; + private InputChannel mChannel; + private TvAdEventReceiver mReceiver; + + public ITvAdSessionWrapper( + Context context, TvAdService.Session mSessionImpl, InputChannel channel) { + this.mSessionImpl = mSessionImpl; + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mChannel = channel; + if (channel != null) { + mReceiver = new TvAdEventReceiver(channel, context.getMainLooper()); + } + } + + @Override + public void release() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); + } + + + @Override + public void executeMessage(Message msg) { + if (mSessionImpl == null) { + return; + } + + long startTime = System.nanoTime(); + switch (msg.what) { + case DO_RELEASE: { + mSessionImpl.release(); + mSessionImpl = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } + break; + } + case DO_SET_SURFACE: { + mSessionImpl.setSurface((Surface) msg.obj); + break; + } + case DO_DISPATCH_SURFACE_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.dispatchSurfaceChanged( + (Integer) args.argi1, (Integer) args.argi2, (Integer) args.argi3); + args.recycle(); + break; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + break; + } + } + long durationMs = (System.nanoTime() - startTime) / (1000 * 1000); + if (durationMs > EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS) { + Log.w(TAG, "Handling message (" + msg.what + ") took too long time (duration=" + + durationMs + "ms)"); + if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) { + // TODO: handle timeout + } + } + + } + + @Override + public void startAdService() throws RemoteException { + + } + + @Override + public void setSurface(Surface surface) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); + } + + @Override + public void dispatchSurfaceChanged(int format, int width, int height) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0)); + } + + private final class TvAdEventReceiver extends InputEventReceiver { + TvAdEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mSessionImpl.dispatchInputEvent(event, this); + if (handled != TvAdManager.Session.DISPATCH_IN_PROGRESS) { + finishInputEvent( + event, handled == TvAdManager.Session.DISPATCH_HANDLED); + } + } + } +} diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 2b52c4b107b6..9c7505197dec 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -17,12 +17,30 @@ package android.media.tv.ad; import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; +import android.media.tv.TvInputManager; import android.media.tv.flags.Flags; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.util.Pools; +import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; +import android.view.Surface; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; /** * Central system API to the overall client-side TV AD architecture, which arbitrates interaction @@ -37,10 +55,163 @@ public class TvAdManager { private final ITvAdManager mService; private final int mUserId; + // A mapping from the sequence number of a session to its SessionCallbackRecord. + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap = + new SparseArray<>(); + + // @GuardedBy("mLock") + private final List<TvAdServiceCallbackRecord> mCallbackRecords = new ArrayList<>(); + + // A sequence number for the next session to be created. Should be protected by a lock + // {@code mSessionCallbackRecordMap}. + private int mNextSeq; + + private final Object mLock = new Object(); + private final ITvAdClient mClient; + /** @hide */ public TvAdManager(ITvAdManager service, int userId) { mService = service; mUserId = userId; + mClient = new ITvAdClient.Stub() { + @Override + public void onSessionCreated(String serviceId, IBinder token, InputChannel channel, + int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for " + token); + return; + } + Session session = null; + if (token != null) { + session = new Session(token, channel, mService, mUserId, seq, + mSessionCallbackRecordMap); + } else { + mSessionCallbackRecordMap.delete(seq); + } + record.postSessionCreated(session); + } + } + + @Override + public void onSessionReleased(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + mSessionCallbackRecordMap.delete(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq:" + seq); + return; + } + record.mSession.releaseInternal(); + record.postSessionReleased(); + } + } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postLayoutSurface(left, top, right, bottom); + } + } + + }; + + ITvAdManagerCallback managerCallback = + new ITvAdManagerCallback.Stub() { + @Override + public void onAdServiceAdded(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceAdded(serviceId); + } + } + } + + @Override + public void onAdServiceRemoved(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceRemoved(serviceId); + } + } + } + + @Override + public void onAdServiceUpdated(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceUpdated(serviceId); + } + } + } + }; + try { + if (mService != null) { + mService.registerCallback(managerCallback, mUserId); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the complete list of TV AD service on the system. + * + * @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta + * information. + * @hide + */ + @NonNull + public List<TvAdServiceInfo> getTvAdServiceList() { + try { + return mService.getTvAdServiceList(mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a {@link Session} for a given TV AD service. + * + * <p>The number of sessions that can be created at the same time is limited by the capability + * of the given AD service. + * + * @param serviceId The ID of the AD service. + * @param callback A callback used to receive the created session. + * @param handler A {@link Handler} that the session creation will be delivered to. + * @hide + */ + public void createSession( + @NonNull String serviceId, + @NonNull String type, + @NonNull final TvAdManager.SessionCallback callback, + @NonNull Handler handler) { + createSessionInternal(serviceId, type, callback, handler); + } + + private void createSessionInternal(String serviceId, String type, + TvAdManager.SessionCallback callback, Handler handler) { + Preconditions.checkNotNull(serviceId); + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(handler); + TvAdManager.SessionCallbackRecord + record = new TvAdManager.SessionCallbackRecord(callback, handler); + synchronized (mSessionCallbackRecordMap) { + int seq = mNextSeq++; + mSessionCallbackRecordMap.put(seq, record); + try { + mService.createSession(mClient, serviceId, type, seq, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -48,14 +219,121 @@ public class TvAdManager { * @hide */ public static final class Session { - private final IBinder mToken; + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; private final ITvAdManager mService; private final int mUserId; + private final int mSeq; + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; - private Session(IBinder token, ITvAdManager service, int userId) { + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); + + private TvInputManager.Session mInputSession; + private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20); + private TvInputEventSender mSender; + private InputChannel mInputChannel; + private IBinder mToken; + + private Session(IBinder token, InputChannel channel, ITvAdManager service, int userId, + int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { mToken = token; + mInputChannel = channel; mService = service; mUserId = userId; + mSeq = seq; + mSessionCallbackRecordMap = sessionCallbackRecordMap; + } + + /** + * Releases this session. + */ + public void release() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.releaseSession(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + releaseInternal(); + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render AD. + */ + public void setSurface(Surface surface) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Notifies of any structural changes (format or size) of the surface passed in + * {@link #setSurface}. + * + * @param format The new PixelFormat of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void dispatchSurfaceChanged(int format, int width, int height) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private void releaseInternal() { + mToken = null; + synchronized (mHandler) { + if (mInputChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mInputChannel.dispose(); + mInputChannel = null; + } + } + synchronized (mSessionCallbackRecordMap) { + mSessionCallbackRecordMap.delete(mSeq); + } } void startAdService() { @@ -69,5 +347,324 @@ public class TvAdManager { throw e.rethrowFromSystemServer(); } } + + private final class InputEventHandler extends Handler { + public static final int MSG_SEND_INPUT_EVENT = 1; + public static final int MSG_TIMEOUT_INPUT_EVENT = 2; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mEventHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mEventHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mInputChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for session to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token A token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + void onFinishedInputEvent(Object token, boolean handled); + } + + private final class TvInputEventSender extends InputEventSender { + TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mEventToken; + public Session.FinishedInputEventCallback mCallback; + public Handler mEventHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mEventToken = null; + mCallback = null; + mEventHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mEventToken, mHandled); + + synchronized (mEventHandler) { + recyclePendingEventLocked(this); + } + } + } + } + + + /** + * Interface used to receive the created session. + * @hide + */ + public abstract static class SessionCallback { + /** + * This is called after {@link TvAdManager#createSession} has been processed. + * + * @param session A {@link TvAdManager.Session} instance created. This can be + * {@code null} if the creation request failed. + */ + public void onSessionCreated(@Nullable Session session) { + } + + /** + * This is called when {@link TvAdManager.Session} is released. + * This typically happens when the process hosting the session has crashed or been killed. + * + * @param session the {@link TvAdManager.Session} instance released. + */ + public void onSessionReleased(@NonNull Session session) { + } + + /** + * This is called when {@link TvAdService.Session#layoutSurface} is called to + * change the layout of surface. + * + * @param session A {@link TvAdManager.Session} associated with this callback. + * @param left Left position. + * @param top Top position. + * @param right Right position. + * @param bottom Bottom position. + */ + public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { + } + + } + + /** + * Callback used to monitor status of the TV AD service. + * @hide + */ + public abstract static class TvAdServiceCallback { + /** + * This is called when a TV AD service is added to the system. + * + * <p>Normally it happens when the user installs a new TV AD service package that implements + * {@link TvAdService} interface. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceAdded(@NonNull String serviceId) { + } + + /** + * This is called when a TV AD service is removed from the system. + * + * <p>Normally it happens when the user uninstalls the previously installed TV AD service + * package. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceRemoved(@NonNull String serviceId) { + } + + /** + * This is called when a TV AD service is updated on the system. + * + * <p>Normally it happens when a previously installed TV AD service package is re-installed + * or a newer version of the package exists becomes available/unavailable. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceUpdated(@NonNull String serviceId) { + } + + } + + private static final class SessionCallbackRecord { + private final SessionCallback mSessionCallback; + private final Handler mHandler; + private Session mSession; + + SessionCallbackRecord(SessionCallback sessionCallback, Handler handler) { + mSessionCallback = sessionCallback; + mHandler = handler; + } + + void postSessionCreated(final Session session) { + mSession = session; + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionCreated(session); + } + }); + } + + void postSessionReleased() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionReleased(mSession); + } + }); + } + + void postLayoutSurface(final int left, final int top, final int right, + final int bottom) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom); + } + }); + } + } + + private static final class TvAdServiceCallbackRecord { + private final TvAdServiceCallback mCallback; + private final Executor mExecutor; + + TvAdServiceCallbackRecord(TvAdServiceCallback callback, Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + public TvAdServiceCallback getCallback() { + return mCallback; + } + + public void postAdServiceAdded(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceAdded(serviceId); + } + }); + } + + public void postAdServiceRemoved(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceRemoved(serviceId); + } + }); + } + + public void postAdServiceUpdated(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceUpdated(serviceId); + } + }); + } } } diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 6897a78647c2..699570397e34 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -16,8 +16,37 @@ package android.media.tv.ad; +import android.annotation.CallSuper; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.os.SomeArgs; + +import java.util.ArrayList; +import java.util.List; /** * The TvAdService class represents a TV client-side advertisement service. @@ -36,9 +65,123 @@ public abstract class TvAdService extends Service { public static final String SERVICE_META_DATA = "android.media.tv.ad.service"; /** + * This is the interface name that a service implementing a TV AD service should + * say that it supports -- that is, this is the action it uses for its intent filter. To be + * supported, the service must also require the + * android.Manifest.permission#BIND_TV_AD_SERVICE permission so that other + * applications cannot abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService"; + + private final Handler mServiceHandler = new ServiceHandler(); + private final RemoteCallbackList<ITvAdServiceCallback> mCallbacks = new RemoteCallbackList<>(); + + @Override + @Nullable + public final IBinder onBind(@NonNull Intent intent) { + ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() { + @Override + public void registerCallback(ITvAdServiceCallback cb) { + if (cb != null) { + mCallbacks.register(cb); + } + } + + @Override + public void unregisterCallback(ITvAdServiceCallback cb) { + if (cb != null) { + mCallbacks.unregister(cb); + } + } + + @Override + public void createSession(InputChannel channel, ITvAdSessionCallback cb, + String serviceId, String type) { + if (cb == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = channel; + args.arg2 = cb; + args.arg3 = serviceId; + args.arg4 = type; + mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args) + .sendToTarget(); + } + + @Override + public void sendAppLinkCommand(Bundle command) { + onAppLinkCommand(command); + } + }; + return tvAdServiceBinder; + } + + /** + * Called when app link command is received. + */ + public void onAppLinkCommand(@NonNull Bundle command) { + } + + + /** + * Returns a concrete implementation of {@link Session}. + * + * <p>May return {@code null} if this TV AD service fails to create a session for some + * reason. + * + * @param serviceId The ID of the TV AD associated with the session. + * @param type The type of the TV AD associated with the session. + */ + @Nullable + public abstract Session onCreateSession(@NonNull String serviceId, @NonNull String type); + + /** * Base class for derived classes to implement to provide a TV AD session. */ public abstract static class Session implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); + + private final Object mLock = new Object(); + // @GuardedBy("mLock") + private ITvAdSessionCallback mSessionCallback; + // @GuardedBy("mLock") + private final List<Runnable> mPendingActions = new ArrayList<>(); + private final Context mContext; + final Handler mHandler; + private final WindowManager mWindowManager; + private Surface mSurface; + + + /** + * Creates a new Session. + * + * @param context The context of the application + */ + public Session(@NonNull Context context) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mHandler = new Handler(context.getMainLooper()); + } + + /** + * Releases TvAdService session. + */ + public abstract void onRelease(); + + void release() { + onRelease(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + synchronized (mLock) { + mSessionCallback = null; + mPendingActions.clear(); + } + } + /** * Starts TvAdService session. */ @@ -48,21 +191,264 @@ public abstract class TvAdService extends Service { void startAdService() { onStartAdService(); } - } - /** - * Implements the internal ITvAdService interface. - */ - public static class ITvAdSessionWrapper extends ITvAdSession.Stub { - private final Session mSessionImpl; + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + return false; + } + + @Override + public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { + return false; + } - public ITvAdSessionWrapper(Session mSessionImpl) { - this.mSessionImpl = mSessionImpl; + @Override + public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) { + return false; } @Override - public void startAdService() { - mSessionImpl.startAdService(); + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + return false; } + + /** + * Implement this method to handle touch screen motion events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTouchEvent + */ + public boolean onTouchEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Implement this method to handle trackball events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTrackballEvent + */ + public boolean onTrackballEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Implement this method to handle generic motion events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onGenericMotionEvent + */ + public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position + * is relative to the overlay view that sits on top of this surface. + * + * @param left Left position in pixels, relative to the overlay view. + * @param top Top position in pixels, relative to the overlay view. + * @param right Right position in pixels, relative to the overlay view. + * @param bottom Bottom position in pixels, relative to the overlay view. + */ + @CallSuper + public void layoutSurface(final int left, final int top, final int right, + final int bottom) { + if (left > right || top > bottom) { + throw new IllegalArgumentException("Invalid parameter"); + } + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + + ", r=" + right + ", b=" + bottom + ",)"); + } + if (mSessionCallback != null) { + mSessionCallback.onLayoutSurface(left, top, right, bottom); + } + } catch (RemoteException e) { + Log.w(TAG, "error in layoutSurface", e); + } + } + }); + } + + /** + * Called when the application sets the surface. + * + * <p>The TV AD service should render AD UI onto the given surface. When called with + * {@code null}, the AD service should immediately free any references to the currently set + * surface and stop using it. + * + * @param surface The surface to be used for AD UI rendering. Can be {@code null}. + * @return {@code true} if the surface was set successfully, {@code false} otherwise. + */ + public abstract boolean onSetSurface(@Nullable Surface surface); + + /** + * Called after any structural changes (format or size) have been made to the surface passed + * in {@link #onSetSurface}. This method is always called at least once, after + * {@link #onSetSurface} is called with non-null surface. + * + * @param format The new {@link PixelFormat} of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) { + } + + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.dispatch(this, mDispatcherState, this)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + + // TODO: special handlings of navigation keys and media keys + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } + } + // TODO: handle overlay view + return TvAdManager.Session.DISPATCH_NOT_HANDLED; + } + + + private void initialize(ITvAdSessionCallback callback) { + synchronized (mLock) { + mSessionCallback = callback; + for (Runnable runnable : mPendingActions) { + runnable.run(); + } + mPendingActions.clear(); + } + } + + /** + * Calls {@link #onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + if (mSurface != null) { + mSurface.release(); + } + mSurface = surface; + // TODO: Handle failure. + } + + /** + * Calls {@link #onSurfaceChanged}. + */ + void dispatchSurfaceChanged(int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width + + ", height=" + height + ")"); + } + onSurfaceChanged(format, width, height); + } + + private void executeOrPostRunnableOnMainThread(Runnable action) { + synchronized (mLock) { + if (mSessionCallback == null) { + // The session is not initialized yet. + mPendingActions.add(action); + } else { + if (mHandler.getLooper().isCurrentThread()) { + action.run(); + } else { + // Posts the runnable if this is not called from the main thread + mHandler.post(action); + } + } + } + } + } + + + @SuppressLint("HandlerLeak") + private final class ServiceHandler extends Handler { + private static final int DO_CREATE_SESSION = 1; + private static final int DO_NOTIFY_SESSION_CREATED = 2; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DO_CREATE_SESSION: { + SomeArgs args = (SomeArgs) msg.obj; + InputChannel channel = (InputChannel) args.arg1; + ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg2; + String serviceId = (String) args.arg3; + String type = (String) args.arg4; + args.recycle(); + TvAdService.Session sessionImpl = onCreateSession(serviceId, type); + if (sessionImpl == null) { + try { + // Failed to create a session. + cb.onSessionCreated(null); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated", e); + } + return; + } + ITvAdSession stub = + new ITvAdSessionWrapper(TvAdService.this, sessionImpl, channel); + + SomeArgs someArgs = SomeArgs.obtain(); + someArgs.arg1 = sessionImpl; + someArgs.arg2 = stub; + someArgs.arg3 = cb; + mServiceHandler.obtainMessage( + DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget(); + return; + } + case DO_NOTIFY_SESSION_CREATED: { + SomeArgs args = (SomeArgs) msg.obj; + Session sessionImpl = (Session) args.arg1; + ITvAdSession stub = (ITvAdSession) args.arg2; + ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg3; + try { + cb.onSessionCreated(stub); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated", e); + } + if (sessionImpl != null) { + sessionImpl.initialize(cb); + } + args.recycle(); + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + } } diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java index ed04f1f9058c..45dc89d838d8 100644 --- a/media/java/android/media/tv/ad/TvAdServiceInfo.java +++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java @@ -24,6 +24,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; +import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Parcel; import android.os.Parcelable; @@ -63,8 +64,7 @@ public final class TvAdServiceInfo implements Parcelable { if (context == null) { throw new IllegalArgumentException("context cannot be null."); } - // TODO: use a constant - Intent intent = new Intent("android.media.tv.ad.TvAdService").setComponent(component); + Intent intent = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component); ResolveInfo resolveInfo = context.getPackageManager().resolveService( intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); if (resolveInfo == null) { @@ -80,6 +80,7 @@ public final class TvAdServiceInfo implements Parcelable { mService = resolveInfo; mId = id; + mTypes.addAll(types); } private TvAdServiceInfo(ResolveInfo service, String id, List<String> types) { @@ -147,9 +148,8 @@ public final class TvAdServiceInfo implements Parcelable { ResolveInfo resolveInfo, Context context, List<String> types) { ServiceInfo serviceInfo = resolveInfo.serviceInfo; PackageManager pm = context.getPackageManager(); - // TODO: use constant for the metadata try (XmlResourceParser parser = - serviceInfo.loadXmlMetaData(pm, "android.media.tv.ad.service")) { + serviceInfo.loadXmlMetaData(pm, TvAdService.SERVICE_META_DATA)) { if (parser == null) { throw new IllegalStateException( "No " + "android.media.tv.ad.service" @@ -171,7 +171,15 @@ public final class TvAdServiceInfo implements Parcelable { + XML_START_TAG_NAME + " tag for " + serviceInfo.name); } - // TODO: parse attributes + TypedArray sa = resources.obtainAttributes(attrs, + com.android.internal.R.styleable.TvAdService); + CharSequence[] textArr = sa.getTextArray( + com.android.internal.R.styleable.TvAdService_adServiceTypes); + for (CharSequence cs : textArr) { + types.add(cs.toString().toLowerCase()); + } + + sa.recycle(); } catch (IOException | XmlPullParserException e) { throw new IllegalStateException( "Failed reading meta-data for " + serviceInfo.packageName, e); diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index 1a3771a9f24c..5e67fe9f697b 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -16,8 +16,20 @@ package android.media.tv.ad; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.util.AttributeSet; import android.util.Log; +import android.util.Xml; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; import android.view.ViewGroup; /** @@ -28,18 +40,166 @@ public class TvAdView extends ViewGroup { private static final String TAG = "TvAdView"; private static final boolean DEBUG = false; - // TODO: create session + private final TvAdManager mTvAdManager; + + private final Handler mHandler = new Handler(); private TvAdManager.Session mSession; + private MySessionCallback mSessionCallback; + + private final AttributeSet mAttrs; + private final int mDefStyleAttr; + private final XmlResourceParser mParser; + + private SurfaceView mSurfaceView; + private Surface mSurface; + + private boolean mSurfaceChanged; + private int mSurfaceFormat; + private int mSurfaceWidth; + private int mSurfaceHeight; + + private boolean mUseRequestedSurfaceLayout; + private int mSurfaceViewLeft; + private int mSurfaceViewRight; + private int mSurfaceViewTop; + private int mSurfaceViewBottom; + + + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + + ", width=" + width + ", height=" + height + ")"); + } + mSurfaceFormat = format; + mSurfaceWidth = width; + mSurfaceHeight = height; + mSurfaceChanged = true; + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurface = null; + mSurfaceChanged = false; + setSessionSurface(null); + } + }; + + + public TvAdView(@NonNull Context context) { + this(context, null, 0); + } + + public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + int sourceResId = Resources.getAttributeSetSourceResId(attrs); + if (sourceResId != Resources.ID_NULL) { + Log.d(TAG, "Build local AttributeSet"); + mParser = context.getResources().getXml(sourceResId); + mAttrs = Xml.asAttributeSet(mParser); + } else { + Log.d(TAG, "Use passed in AttributeSet"); + mParser = null; + mAttrs = attrs; + } + mDefStyleAttr = defStyleAttr; + resetSurfaceView(); + mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE); + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right + + ", bottom=" + bottom + ",)"); + } + if (mUseRequestedSurfaceLayout) { + mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight, + mSurfaceViewBottom); + } else { + mSurfaceView.layout(0, 0, right - left, bottom - top); + } + } - public TvAdView(Context context) { - super(context, /* attrs = */null, /* defStyleAttr = */0); + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); + int width = mSurfaceView.getMeasuredWidth(); + int height = mSurfaceView.getMeasuredHeight(); + int childState = mSurfaceView.getMeasuredState(); + setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), + resolveSizeAndState(height, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); } @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { + public void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + mSurfaceView.setVisibility(visibility); + } + + private void resetSurfaceView() { + if (mSurfaceView != null) { + mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); + removeView(mSurfaceView); + } + mSurface = null; + mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) { + @Override + protected void updateSurface() { + super.updateSurface(); + }}; + // The surface view's content should be treated as secure all the time. + mSurfaceView.setSecure(true); + mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); + + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.setZOrderMediaOverlay(true); + + addView(mSurfaceView); + } + + private void setSessionSurface(Surface surface) { + if (mSession == null) { + return; + } + mSession.setSurface(surface); + } + + private void dispatchSurfaceChanged(int format, int width, int height) { + if (mSession == null) { + return; + } + //mSession.dispatchSurfaceChanged(format, width, height); + } + + /** + * Prepares the AD service of corresponding {@link TvAdService}. + * + * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId(). + */ + public void prepareAdService(@NonNull String serviceId, @NonNull String type) { if (DEBUG) { - Log.d(TAG, - "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)"); + Log.d(TAG, "prepareAdService"); + } + mSessionCallback = new TvAdView.MySessionCallback(serviceId); + if (mTvAdManager != null) { + mTvAdManager.createSession(serviceId, type, mSessionCallback, mHandler); } } @@ -54,4 +214,75 @@ public class TvAdView extends ViewGroup { mSession.startAdService(); } } + + private class MySessionCallback extends TvAdManager.SessionCallback { + final String mServiceId; + + MySessionCallback(String serviceId) { + mServiceId = serviceId; + } + + @Override + public void onSessionCreated(TvAdManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionCreated()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionCreated - session already created"); + // This callback is obsolete. + if (session != null) { + session.release(); + } + return; + } + mSession = session; + if (session != null) { + // mSurface may not be ready yet as soon as starting an application. + // In the case, we don't send Session.setSurface(null) unnecessarily. + // setSessionSurface will be called in surfaceCreated. + if (mSurface != null) { + setSessionSurface(mSurface); + if (mSurfaceChanged) { + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + } + } else { + // Failed to create + // Todo: forward error to Tv App + mSessionCallback = null; + } + } + + @Override + public void onSessionReleased(TvAdManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionReleased()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionReleased - session not created"); + return; + } + mSessionCallback = null; + mSession = null; + } + + @Override + public void onLayoutSurface( + TvAdManager.Session session, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right=" + + right + ", bottom=" + bottom + ",)"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onLayoutSurface - session not created"); + return; + } + mSurfaceViewLeft = left; + mSurfaceViewTop = top; + mSurfaceViewRight = right; + mSurfaceViewBottom = bottom; + mUseRequestedSurfaceLayout = true; + requestLayout(); + } + } } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index 77391841c6fe..e3dba03d6093 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -48,6 +48,7 @@ oneway interface ITvInteractiveAppClient { void onRequestCurrentChannelLcn(int seq); void onRequestStreamVolume(int seq); void onRequestTrackInfoList(int seq); + void onRequestSelectedTrackInfo(int seq); void onRequestCurrentTvInputId(int seq); void onRequestTimeShiftMode(int seq); void onRequestAvailableSpeeds(int seq); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 41cbe4ae02d0..4316d053a275 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -102,6 +102,8 @@ interface ITvInteractiveAppManager { int UserId); void notifyAdResponse(in IBinder sessionToken, in AdResponse response, int UserId); void notifyAdBufferConsumed(in IBinder sessionToken, in AdBuffer buffer, int userId); + void sendSelectedTrackInfo(in IBinder sessionToken, in List<TvTrackInfo> tracks, + int userId); void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index 052bc3d5adce..ba7cf13a7a1d 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -78,6 +78,7 @@ oneway interface ITvInteractiveAppSession { void notifyBroadcastInfoResponse(in BroadcastInfoResponse response); void notifyAdResponse(in AdResponse response); void notifyAdBufferConsumed(in AdBuffer buffer); + void sendSelectedTrackInfo(in List<TvTrackInfo> tracks); void createMediaView(in IBinder windowToken, in Rect frame); void relayoutMediaView(in Rect frame); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index 9e43e79144fd..416b8f12d5ea 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -50,6 +50,7 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestCurrentTvInputId(); void onRequestTimeShiftMode(); void onRequestAvailableSpeeds(); + void onRequestSelectedTrackInfo(); void onRequestStartRecording(in String requestId, in Uri programUri); void onRequestStopRecording(in String recordingId); void onRequestScheduleRecording(in String requestId, in String inputId, in Uri channelUri, diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 253ade809ece..518b08a93f95 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -102,6 +102,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_NOTIFY_RECORDING_SCHEDULED = 45; private static final int DO_SEND_TIME_SHIFT_MODE = 46; private static final int DO_SEND_AVAILABLE_SPEEDS = 47; + private static final int DO_SEND_SELECTED_TRACK_INFO = 48; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -247,6 +248,10 @@ public class ITvInteractiveAppSessionWrapper args.recycle(); break; } + case DO_SEND_SELECTED_TRACK_INFO: { + mSessionImpl.sendSelectedTrackInfo((List<TvTrackInfo>) msg.obj); + break; + } case DO_NOTIFY_VIDEO_AVAILABLE: { mSessionImpl.notifyVideoAvailable(); break; @@ -526,6 +531,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void sendSelectedTrackInfo(List<TvTrackInfo> tracks) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_SELECTED_TRACK_INFO, tracks)); + } + + @Override public void notifyTracksChanged(List<TvTrackInfo> tracks) { mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_TRACKS_CHANGED, tracks)); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 7cce84a1ee16..bf4379f470d8 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -33,7 +33,6 @@ import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; -import android.media.tv.interactive.TvInteractiveAppService.Session; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -506,6 +505,18 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestSelectedTrackInfo(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestSelectedTrackInfo(); + } + } + + @Override public void onRequestCurrentTvInputId(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); @@ -1209,6 +1220,18 @@ public final class TvInteractiveAppManager { } } + void sendSelectedTrackInfo(@NonNull List<TvTrackInfo> tracks) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendSelectedTrackInfo(mToken, tracks, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + void sendCurrentTvInputId(@Nullable String inputId) { if (mToken == null) { Log.w(TAG, "The session has been already released"); @@ -2108,6 +2131,15 @@ public final class TvInteractiveAppManager { }); } + void postRequestSelectedTrackInfo() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestSelectedTrackInfo(mSession); + } + }); + } + void postRequestCurrentTvInputId() { mHandler.post(new Runnable() { @Override @@ -2378,6 +2410,15 @@ public final class TvInteractiveAppManager { } /** + * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is + * called. + * + * @param session A {@link TvInteractiveAppManager.Session} associated with this callback. + */ + public void onRequestSelectedTrackInfo(Session session) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId} is * called. * diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 241940486a14..79364034ac2a 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -112,7 +112,8 @@ public abstract class TvInteractiveAppService extends Service { PLAYBACK_COMMAND_TYPE_TUNE_PREV, PLAYBACK_COMMAND_TYPE_STOP, PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME, - PLAYBACK_COMMAND_TYPE_SELECT_TRACK + PLAYBACK_COMMAND_TYPE_SELECT_TRACK, + PLAYBACK_COMMAND_TYPE_FREEZE }) public @interface PlaybackCommandType {} @@ -142,8 +143,11 @@ public abstract class TvInteractiveAppService extends Service { * Playback command type: select the given track. */ public static final String PLAYBACK_COMMAND_TYPE_SELECT_TRACK = "select_track"; - - + /** + * Playback command type: freeze the video playback on the current frame. + * @hide + */ + public static final String PLAYBACK_COMMAND_TYPE_FREEZE = "freeze"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -932,6 +936,16 @@ public abstract class TvInteractiveAppService extends Service { @NonNull Bundle data) { } + /** + * Called when the TV App sends the selected track info as a response to + * requestSelectedTrackInfo. + * + * @param tracks + * @hide + */ + public void onSelectedTrackInfo(List<TvTrackInfo> tracks) { + } + @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { return false; @@ -1338,6 +1352,30 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests the currently selected {@link TvTrackInfo} from the TV App. + * + * <p> Normally, track info cannot be synchronized until the channel has + * been changed. This is used when the session of the TIAS is newly + * created and the normal synchronization has not happened yet. + * @hide + */ + @CallSuper + public void requestSelectedTrackInfo() { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "requestSelectedTrackInfo"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestSelectedTrackInfo(); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestSelectedTrackInfo", e); + } + }); + } + + /** * Requests starting of recording * * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to @@ -1781,6 +1819,13 @@ public abstract class TvInteractiveAppService extends Service { onTvMessage(type, data); } + void sendSelectedTrackInfo(List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "notifySelectedTrackInfo (tracks= " + tracks + ")"); + } + onSelectedTrackInfo(tracks); + } + /** * Calls {@link #onAdBufferConsumed}. */ diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index cbaf5e482faa..40a12e4db4cc 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -582,6 +582,20 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Sends the currently selected track info to the TV Interactive App. + * + * @hide + */ + public void sendSelectedTrackInfo(@Nullable List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "sendSelectedTrackInfo"); + } + if (mSession != null) { + mSession.sendSelectedTrackInfo(tracks); + } + } + + /** * Sends current TV input ID to related TV interactive app. * * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is @@ -1197,6 +1211,16 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is + * called. + * + * @param iAppServiceId The ID of the TV interactive app service bound to this view. + * @hide + */ + public void onRequestSelectedTrackInfo(@NonNull String iAppServiceId) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId()} is * called. * @@ -1714,6 +1738,28 @@ public class TvInteractiveAppView extends ViewGroup { } @Override + public void onRequestSelectedTrackInfo(Session session) { + if (DEBUG) { + Log.d(TAG, "onRequestSelectedTrackInfo"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onRequestSelectedTrackInfo - session not created"); + return; + } + synchronized (mCallbackLock) { + if (mCallbackExecutor != null) { + mCallbackExecutor.execute(() -> { + synchronized (mCallbackLock) { + if (mCallback != null) { + mCallback.onRequestSelectedTrackInfo(mIAppServiceId); + } + } + }); + } + } + } + + @Override public void onRequestCurrentTvInputId(Session session) { if (DEBUG) { Log.d(TAG, "onRequestCurrentTvInputId"); diff --git a/media/java/android/media/tv/tuner/Lnb.java b/media/java/android/media/tv/tuner/Lnb.java index f9eaabd800b1..0b9429debb62 100644 --- a/media/java/android/media/tv/tuner/Lnb.java +++ b/media/java/android/media/tv/tuner/Lnb.java @@ -264,6 +264,25 @@ public class Lnb implements AutoCloseable { } } + /* package */ void closeInternal() { + synchronized (mLock) { + if (mIsClosed) { + return; + } + int res = nativeClose(); + if (res != Tuner.RESULT_SUCCESS) { + TunerUtils.throwExceptionForResult(res, "Failed to close LNB"); + } else { + mIsClosed = true; + if (mOwner != null) { + mOwner.releaseLnb(); + mOwner = null; + } + mCallbackMap.clear(); + } + } + } + /** * Sets the LNB's power voltage. * @@ -330,22 +349,7 @@ public class Lnb implements AutoCloseable { public void close() { acquireTRMSLock("close()"); try { - synchronized (mLock) { - if (mIsClosed) { - return; - } - int res = nativeClose(); - if (res != Tuner.RESULT_SUCCESS) { - TunerUtils.throwExceptionForResult(res, "Failed to close LNB"); - } else { - mIsClosed = true; - if (mOwner != null) { - mOwner.releaseLnb(); - mOwner = null; - } - mCallbackMap.clear(); - } - } + closeInternal(); } finally { releaseTRMSLock(); } diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java index 09f09b94ac0d..f28c2c17167f 100644 --- a/media/java/android/media/tv/tuner/Tuner.java +++ b/media/java/android/media/tv/tuner/Tuner.java @@ -30,6 +30,7 @@ import android.content.pm.PackageManager; import android.hardware.tv.tuner.Constant; import android.hardware.tv.tuner.Constant64Bit; import android.hardware.tv.tuner.FrontendScanType; +import android.media.MediaCodec; import android.media.tv.TvInputService; import android.media.tv.tuner.dvr.DvrPlayback; import android.media.tv.tuner.dvr.DvrRecorder; @@ -272,8 +273,12 @@ public class Tuner implements AutoCloseable { try { System.loadLibrary("media_tv_tuner"); nativeInit(); + // Load and initialize MediaCodec to avoid flaky cts test result. + Class.forName(MediaCodec.class.getName()); } catch (UnsatisfiedLinkError e) { Log.d(TAG, "tuner JNI library not found!"); + } catch (ClassNotFoundException e) { + Log.e(TAG, "MediaCodec class not found!", e); } } @@ -914,7 +919,7 @@ public class Tuner implements AutoCloseable { if (DEBUG) { Log.d(TAG, "calling mLnb.close() : " + mClientId); } - mLnb.close(); + mLnb.closeInternal(); } else { if (DEBUG) { Log.d(TAG, "NOT calling mLnb.close() : " + mClientId); @@ -2348,6 +2353,7 @@ public class Tuner implements AutoCloseable { @Nullable public Lnb openLnbByName(@NonNull String name, @CallbackExecutor @NonNull Executor executor, @NonNull LnbCallback cb) { + acquireTRMSLock("openLnbByName"); mLnbLock.lock(); try { Objects.requireNonNull(name, "LNB name must not be null"); @@ -2356,7 +2362,7 @@ public class Tuner implements AutoCloseable { Lnb newLnb = nativeOpenLnbByName(name); if (newLnb != null) { if (mLnb != null) { - mLnb.close(); + mLnb.closeInternal(); mLnbHandle = null; } mLnb = newLnb; @@ -2367,6 +2373,7 @@ public class Tuner implements AutoCloseable { } return mLnb; } finally { + releaseTRMSLock(); mLnbLock.unlock(); } } @@ -2784,8 +2791,8 @@ public class Tuner implements AutoCloseable { } } + // Must be called while TRMS lock is being held /* package */ void releaseLnb() { - acquireTRMSLock("releaseLnb()"); mLnbLock.lock(); try { if (mLnbHandle != null) { @@ -2802,7 +2809,6 @@ public class Tuner implements AutoCloseable { } mLnb = null; } finally { - releaseTRMSLock(); mLnbLock.unlock(); } } diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 3fcb8713672f..00b0e57c09ea 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -967,7 +967,7 @@ void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) { ScopedLocalRef<jobject> filter(env); { android::Mutex::Autolock autoLock(mLock); - if (env->IsSameObject(filter.get(), nullptr)) { + if (env->IsSameObject(mFilterObj, nullptr)) { ALOGE("FilterClientCallbackImpl::onFilterStatus:" "Filter object has been freed. Ignoring callback."); return; diff --git a/nfc/Android.bp b/nfc/Android.bp index 87da299464e4..7136866d536f 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -68,7 +68,7 @@ java_sdk_library { ], jarjar_rules: ":nfc-jarjar-rules", lint: { - strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", }, apex_available: [ "//apex_available:platform", diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 24c145f890c4..7573474f65b4 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -204,6 +204,7 @@ package android.nfc.cardemulation { method public boolean isDefaultServiceForAid(android.content.ComponentName, String); method public boolean isDefaultServiceForCategory(android.content.ComponentName, String); method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String); method public boolean removeAidsForService(android.content.ComponentName, String); method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean setOffHostForService(@NonNull android.content.ComponentName, @NonNull String); method public boolean setPreferredService(android.app.Activity, android.content.ComponentName); diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl index 286cf2890eea..bec62c5b1b82 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -97,4 +97,7 @@ interface INfcAdapter WlcLDeviceInfo getWlcLDeviceInfo(); void updateDiscoveryTechnology(IBinder b, int pollFlags, int listenFlags); + + void notifyPollingLoop(in Bundle frame); + void notifyHceDeactivated(); } diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl index f4b46046bc3e..791bd8c9e6f4 100644 --- a/nfc/java/android/nfc/INfcCardEmulation.aidl +++ b/nfc/java/android/nfc/INfcCardEmulation.aidl @@ -32,6 +32,7 @@ interface INfcCardEmulation boolean setDefaultForNextTap(int userHandle, in ComponentName service); boolean setServiceObserveModeDefault(int userId, in android.content.ComponentName service, boolean enable); boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup); + boolean registerPollingLoopFilterForService(int userHandle, in ComponentName service, in String pollingLoopFilter); boolean setOffHostForService(int userHandle, in ComponentName service, in String offHostSecureElement); boolean unsetOffHostForService(int userHandle, in ComponentName service); AidGroup getAidGroupForService(int userHandle, in ComponentName service, String category); diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 8219d2f873de..68c16e69f3af 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -26,6 +26,7 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.Activity; import android.app.PendingIntent; @@ -2752,6 +2753,64 @@ public final class NfcAdapter { } } + /** + * Notifies the system of a new polling loop. + * + * @param frame is the new frame. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void notifyPollingLoop(@NonNull Bundle frame) { + try { + if (sService == null) { + attemptDeadServiceRecovery(null); + } + sService.notifyPollingLoop(frame); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return; + } + try { + sService.notifyPollingLoop(frame); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + } + } + + /** + * Notifies the system of a an HCE session being deactivated. + * * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void notifyHceDeactivated() { + try { + if (sService == null) { + attemptDeadServiceRecovery(null); + } + sService.notifyHceDeactivated(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return; + } + try { + sService.notifyHceDeactivated(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + } + } + /** * Sets NFC charging feature. * <p>This API is for the Settings application. @@ -2767,6 +2826,7 @@ public final class NfcAdapter { } try { return sService.enableWlc(enable); + } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index 41dee3ab035c..426c5aac487b 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -52,6 +52,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; @@ -102,6 +103,8 @@ public final class ApduServiceInfo implements Parcelable { */ private final HashMap<String, AidGroup> mDynamicAidGroups; + private final ArrayList<String> mPollingLoopFilters; + /** * Whether this service should only be started when the device is unlocked. */ @@ -169,6 +172,7 @@ public final class ApduServiceInfo implements Parcelable { this.mDescription = description; this.mStaticAidGroups = new HashMap<String, AidGroup>(); this.mDynamicAidGroups = new HashMap<String, AidGroup>(); + this.mPollingLoopFilters = new ArrayList<String>(); this.mOffHostName = offHost; this.mStaticOffHostName = staticOffHost; this.mOnHost = onHost; @@ -282,6 +286,7 @@ public final class ApduServiceInfo implements Parcelable { mStaticAidGroups = new HashMap<String, AidGroup>(); mDynamicAidGroups = new HashMap<String, AidGroup>(); + mPollingLoopFilters = new ArrayList<String>(); mOnHost = onHost; final int depth = parser.getDepth(); @@ -364,6 +369,15 @@ public final class ApduServiceInfo implements Parcelable { Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); } a.recycle(); + } else if (eventType == XmlPullParser.START_TAG + && "polling-loop-filter".equals(tagName) && currentGroup == null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.PollingLoopFilter); + String plf = + a.getString(com.android.internal.R.styleable.PollingLoopFilter_name) + .toUpperCase(Locale.ROOT); + mPollingLoopFilters.add(plf); + a.recycle(); } } } catch (NameNotFoundException e) { @@ -420,6 +434,16 @@ public final class ApduServiceInfo implements Parcelable { } /** + * Returns the current polling loop filters for this service. + * @return List of polling loop filters. + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + @NonNull + public List<String> getPollingLoopFilters() { + return mPollingLoopFilters; + } + + /** * Returns a consolidated list of AIDs with prefixes from the AID groups * registered by this service. Note that if a service has both * a static (manifest-based) AID group for a category and a dynamic @@ -596,6 +620,26 @@ public final class ApduServiceInfo implements Parcelable { } /** + * Add a Polling Loop Filter. Custom NFC polling frames that match this filter will be + * delivered to {@link HostApduService#processPollingFrames(List)}. + * @param pollingLoopFilter this polling loop filter to add. + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void addPollingLoopFilter(@NonNull String pollingLoopFilter) { + mPollingLoopFilters.add(pollingLoopFilter.toUpperCase(Locale.ROOT)); + } + + /** + * Remove a Polling Loop Filter. Custom NFC polling frames that match this filter will no + * longer be delivered to {@link HostApduService#processPollingFrames(List)}. + * @param pollingLoopFilter this polling loop filter to add. + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void removePollingLoopFilter(@NonNull String pollingLoopFilter) { + mPollingLoopFilters.remove(pollingLoopFilter.toUpperCase(Locale.ROOT)); + } + + /** * Sets the off host Secure Element. * @param offHost Secure Element to set. Only accept strings with prefix SIM or prefix eSE. * Ref: GSMA TS.26 - NFC Handset Requirements diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 81eab71fe080..0943392a68ad 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -57,6 +57,8 @@ import java.util.regex.Pattern; */ public final class CardEmulation { private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?"); + private static final Pattern PLF_PATTERN = Pattern.compile("[0-9A-Fa-f]{1,32}"); + static final String TAG = "CardEmulation"; /** @@ -353,6 +355,34 @@ public final class CardEmulation { } /** + * Register a polling loop filter for a HostApduService. + * @param service The HostApduService to register the filter for. + * @param pollingLoopFilter The filter to register. + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public boolean registerPollingLoopFilterForService(@NonNull ComponentName service, + @NonNull String pollingLoopFilter) { + try { + return sService.registerPollingLoopFilterForService(mContext.getUser().getIdentifier(), + service, pollingLoopFilter); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.registerPollingLoopFilterForService( + mContext.getUser().getIdentifier(), service, pollingLoopFilter); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** * Registers a list of AIDs for a specific category for the * specified service. * @@ -933,6 +963,24 @@ public final class CardEmulation { } /** + * Tests the validity of the polling loop filter. + * @param pollingLoopFilter The polling loop filter to test. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public static boolean isValidPollingLoopFilter(@NonNull String pollingLoopFilter) { + // Verify hex characters + if (!PLF_PATTERN.matcher(pollingLoopFilter).matches()) { + Log.e(TAG, "Polling Loop Filter " + pollingLoopFilter + + " is not a valid Polling Loop Filter."); + return false; + } + + return true; + } + + /** * A valid AID according to ISO/IEC 7816-4: * <ul> * <li>Has >= 5 bytes and <=16 bytes (>=10 hex chars and <= 32 hex chars) diff --git a/nfc/lint-baseline.xml b/nfc/lint-baseline.xml new file mode 100644 index 000000000000..1dfdd29e480a --- /dev/null +++ b/nfc/lint-baseline.xml @@ -0,0 +1,213 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01"> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `new android.nfc.cardemulation.AidGroup`" + errorLine1=" AidGroup aidGroup = new AidGroup(aids, category);" + errorLine2=" ~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="377" + column="29"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.AidGroup#getAids`" + errorLine1=" return (group != null ? group.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="537" + column="43"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.AidGroup#getAids`" + errorLine1=" return (group != null ? group.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="547" + column="47"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getAids`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="714" + column="55"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getAids`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="724" + column="59"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#isOnHost`" + errorLine1=" if (!serviceInfo.isOnHost()) {" + errorLine2=" ~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="755" + column="34"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=" return serviceInfo.getOffHostSecureElement() == null ?" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="756" + column="40"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=' "OffHost" : serviceInfo.getOffHostSecureElement();' + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="757" + column="53"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#isOnHost`" + errorLine1=" if (!serviceInfo.isOnHost()) {" + errorLine2=" ~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="772" + column="38"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=" return serviceInfo.getOffHostSecureElement() == null ?" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="773" + column="44"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=' "Offhost" : serviceInfo.getOffHostSecureElement();' + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="774" + column="57"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getDescription`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getDescription() : null);" + errorLine2=" ~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="798" + column="55"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getDescription`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getDescription() : null);" + errorLine2=" ~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="808" + column="59"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="1032" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="1066" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" resumed = activity.isResumed();" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/NfcActivityManager.java" + line="124" + column="32"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/NfcAdapter.java" + line="2457" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java" + line="315" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java" + line="351" + column="23"/> + </issue> + +</issues>
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java index 2da8c8c69ff8..221ca4fd1c66 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java @@ -32,6 +32,7 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Flags; import android.os.Process; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -131,7 +132,7 @@ public class UninstallAlertDialogFragment extends DialogFragment implements final boolean isUpdate = ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); final boolean isArchive = - android.content.pm.Flags.archiving() && ( + isArchivingEnabled() && ( (dialogInfo.deleteFlags & PackageManager.DELETE_ARCHIVE) != 0); final UserHandle myUserHandle = Process.myUserHandle(); UserManager userManager = getContext().getSystemService(UserManager.class); @@ -242,6 +243,11 @@ public class UninstallAlertDialogFragment extends DialogFragment implements return dialogBuilder.create(); } + private static boolean isArchivingEnabled() { + return android.content.pm.Flags.archiving() + || SystemProperties.getBoolean("pm.archiving.enabled", false); + } + private boolean isCloneProfile(UserHandle userHandle) { UserManager customUserManager = getContext() .createContextAsUser(UserHandle.of(userHandle.getIdentifier()), 0) diff --git a/packages/SettingsLib/ActionBarShadow/Android.bp b/packages/SettingsLib/ActionBarShadow/Android.bp index 6f9445874fce..77cbb00427dc 100644 --- a/packages/SettingsLib/ActionBarShadow/Android.bp +++ b/packages/SettingsLib/ActionBarShadow/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibActionBarShadow", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp index 122855561751..c36b82d175e2 100644 --- a/packages/SettingsLib/ActionButtonsPreference/Android.bp +++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibActionButtonsPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp index 41de29a93e51..838a9e505ece 100644 --- a/packages/SettingsLib/ActivityEmbedding/Android.bp +++ b/packages/SettingsLib/ActivityEmbedding/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibActivityEmbedding", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/AdaptiveIcon/Android.bp b/packages/SettingsLib/AdaptiveIcon/Android.bp index 044ba872f3e5..67b6fb5f2ed9 100644 --- a/packages/SettingsLib/AdaptiveIcon/Android.bp +++ b/packages/SettingsLib/AdaptiveIcon/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibAdaptiveIcon", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 5da4b9518a31..c2cb75709b45 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -9,6 +9,9 @@ package { android_library { name: "SettingsLib", + defaults: [ + "SettingsLintDefaults", + ], static_libs: [ "androidx.localbroadcastmanager_localbroadcastmanager", @@ -60,8 +63,15 @@ android_library { "src/**/*.java", "src/**/*.kt", ], +} + +// defaults for lint option +java_defaults { + name: "SettingsLintDefaults", lint: { - extra_check_modules: ["SettingsLibLintChecker"], + extra_check_modules: [ + "SettingsLibLintChecker", + ], }, } diff --git a/packages/SettingsLib/AppPreference/Android.bp b/packages/SettingsLib/AppPreference/Android.bp index 69b9d44fe16f..c5b2ef686688 100644 --- a/packages/SettingsLib/AppPreference/Android.bp +++ b/packages/SettingsLib/AppPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibAppPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/BannerMessagePreference/Android.bp b/packages/SettingsLib/BannerMessagePreference/Android.bp index da91344242a1..07290de8661e 100644 --- a/packages/SettingsLib/BannerMessagePreference/Android.bp +++ b/packages/SettingsLib/BannerMessagePreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibBannerMessagePreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/BarChartPreference/Android.bp b/packages/SettingsLib/BarChartPreference/Android.bp index be1e0cf8ab4f..448ed56a7f28 100644 --- a/packages/SettingsLib/BarChartPreference/Android.bp +++ b/packages/SettingsLib/BarChartPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibBarChartPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/ButtonPreference/Android.bp b/packages/SettingsLib/ButtonPreference/Android.bp index 35572fad55a2..0382829b2652 100644 --- a/packages/SettingsLib/ButtonPreference/Android.bp +++ b/packages/SettingsLib/ButtonPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibButtonPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp index 70f7554d5e53..87ec0b8d46fb 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibCollapsingToolbarBaseActivity", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/DisplayUtils/Android.bp b/packages/SettingsLib/DisplayUtils/Android.bp index eab35a11d7d6..279bb70d81bf 100644 --- a/packages/SettingsLib/DisplayUtils/Android.bp +++ b/packages/SettingsLib/DisplayUtils/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibDisplayUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/EntityHeaderWidgets/Android.bp b/packages/SettingsLib/EntityHeaderWidgets/Android.bp index 17b662c60227..83f81c60c856 100644 --- a/packages/SettingsLib/EntityHeaderWidgets/Android.bp +++ b/packages/SettingsLib/EntityHeaderWidgets/Android.bp @@ -10,13 +10,16 @@ package { android_library { name: "SettingsLibEntityHeaderWidgets", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], static_libs: [ - "androidx.annotation_annotation", - "SettingsLibSettingsTheme" + "androidx.annotation_annotation", + "SettingsLibSettingsTheme", ], sdk_version: "system_current", diff --git a/packages/SettingsLib/FooterPreference/Android.bp b/packages/SettingsLib/FooterPreference/Android.bp index b45cd65467d2..d1ad80d5a4d7 100644 --- a/packages/SettingsLib/FooterPreference/Android.bp +++ b/packages/SettingsLib/FooterPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibFooterPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/HelpUtils/Android.bp b/packages/SettingsLib/HelpUtils/Android.bp index 041fce254b72..284106e96fda 100644 --- a/packages/SettingsLib/HelpUtils/Android.bp +++ b/packages/SettingsLib/HelpUtils/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibHelpUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/IllustrationPreference/Android.bp b/packages/SettingsLib/IllustrationPreference/Android.bp index 4d4759b99f15..6407810367cf 100644 --- a/packages/SettingsLib/IllustrationPreference/Android.bp +++ b/packages/SettingsLib/IllustrationPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibIllustrationPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/LayoutPreference/Android.bp b/packages/SettingsLib/LayoutPreference/Android.bp index 53ded2385634..8cf636ac8de3 100644 --- a/packages/SettingsLib/LayoutPreference/Android.bp +++ b/packages/SettingsLib/LayoutPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibLayoutPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/MainSwitchPreference/Android.bp b/packages/SettingsLib/MainSwitchPreference/Android.bp index 010a6ce9d4d9..b984aaf050d5 100644 --- a/packages/SettingsLib/MainSwitchPreference/Android.bp +++ b/packages/SettingsLib/MainSwitchPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibMainSwitchPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp index 155ed2e091f8..6dc07b29a510 100644 --- a/packages/SettingsLib/ProfileSelector/Android.bp +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibProfileSelector", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/RestrictedLockUtils/Android.bp b/packages/SettingsLib/RestrictedLockUtils/Android.bp index 3b04bd99e0f4..8d722eba49bf 100644 --- a/packages/SettingsLib/RestrictedLockUtils/Android.bp +++ b/packages/SettingsLib/RestrictedLockUtils/Android.bp @@ -16,6 +16,9 @@ filegroup { android_library { name: "SettingsLibRestrictedLockUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/SchedulesProvider/Android.bp b/packages/SettingsLib/SchedulesProvider/Android.bp index 22e4e94b80b1..c0fc741e7447 100644 --- a/packages/SettingsLib/SchedulesProvider/Android.bp +++ b/packages/SettingsLib/SchedulesProvider/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSchedulesProvider", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/SearchProvider/Android.bp b/packages/SettingsLib/SearchProvider/Android.bp index c385d385dcc9..61ed65cbe46f 100644 --- a/packages/SettingsLib/SearchProvider/Android.bp +++ b/packages/SettingsLib/SearchProvider/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSearchProvider", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp index 702387ecadab..2fe446d24b34 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp +++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSelectorWithWidgetPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/SettingsSpinner/Android.bp b/packages/SettingsLib/SettingsSpinner/Android.bp index 0eec50563a75..8fed61fd3f13 100644 --- a/packages/SettingsLib/SettingsSpinner/Android.bp +++ b/packages/SettingsLib/SettingsSpinner/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSettingsSpinner", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp index 06493c056203..e04af6c1ab11 100644 --- a/packages/SettingsLib/SettingsTransition/Android.bp +++ b/packages/SettingsLib/SettingsTransition/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSettingsTransition", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index 8b136da04405..6f9556f91bd6 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.6.0-beta02" + extra["jetpackComposeVersion"] = "1.6.0-rc01" } subprojects { diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml index ed9028472fd0..df5644b8aad0 100644 --- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml @@ -74,8 +74,7 @@ android:exported="false"> </provider> <activity - android:name="com.android.settingslib.spa.gallery.SpaDialogActivity" - android:excludeFromRecents="true" + android:name="com.android.settingslib.spa.gallery.GalleryDialogActivity" android:exported="true" android:theme="@style/Theme.SpaLib.Dialog"> </activity> diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDialogActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDialogActivity.kt new file mode 100644 index 000000000000..e22ed355bff1 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDialogActivity.kt @@ -0,0 +1,45 @@ +/* + * 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.settingslib.spa.gallery + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import com.android.settingslib.spa.SpaBaseDialogActivity +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon + +class GalleryDialogActivity : SpaBaseDialogActivity() { + @Composable + override fun Content() { + SettingsAlertDialogWithIcon( + onDismissRequest = { finish() }, + confirmButton = AlertDialogButton("confirm") { finish() }, + dismissButton = AlertDialogButton("dismiss") { finish() }, + title = "title", + text = { + Text( + "text", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + ) + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt deleted file mode 100644 index 8b80fe241639..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.WarningAmber -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import com.android.settingslib.spa.framework.common.LogCategory -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.widget.dialog.getDialogWidth - - -class SpaDialogActivity : ComponentActivity() { - private val spaEnvironment get() = SpaEnvironmentFactory.instance - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) - setContent { - SettingsTheme { - Content() - } - } - } - - @Composable - fun Content() { - var openAlertDialog by remember { mutableStateOf(false) } - AlertDialog(openAlertDialog) - LaunchedEffect(key1 = Unit) { - openAlertDialog = true - } - } - - @Composable - fun AlertDialog(openAlertDialog: Boolean) { - when { - openAlertDialog -> { - AlertDialogExample( - onDismissRequest = { finish() }, - onConfirmation = { finish() }, - dialogTitle = intent.getStringExtra(DIALOG_TITLE) ?: DIALOG_TITLE, - dialogText = intent.getStringExtra(DIALOG_TEXT) ?: DIALOG_TEXT, - icon = Icons.Default.WarningAmber - ) - } - } - } - - @Composable - fun AlertDialogExample( - onDismissRequest: () -> Unit, - onConfirmation: () -> Unit, - dialogTitle: String, - dialogText: String, - icon: ImageVector, - ) { - AlertDialog( - modifier = Modifier.width(getDialogWidth()), - icon = { - Icon(icon, contentDescription = null) - }, - title = { - Text(text = dialogTitle) - }, - text = { - Text(text = dialogText) - }, - onDismissRequest = { - onDismissRequest() - }, - dismissButton = { - OutlinedButton( - onClick = { - onDismissRequest() - } - ) { - Text(intent.getStringExtra(DISMISS_TEXT) ?: DISMISS_TEXT) - } - }, - confirmButton = { - Button( - onClick = { - onConfirmation() - }, - ) { - Text(intent.getStringExtra(CONFIRM_TEXT) ?: CONFIRM_TEXT) - } - } - ) - } - - companion object { - private const val TAG = "SpaDialogActivity" - private const val DIALOG_TITLE = "dialogTitle" - private const val DIALOG_TEXT = "dialogText" - private const val CONFIRM_TEXT = "confirmText" - private const val DISMISS_TEXT = "dismissText" - } -} diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index 9703c347859c..1f78a9c3ac07 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,7 +15,7 @@ # [versions] -agp = "8.2.0" +agp = "8.2.1" compose-compiler = "1.5.1" dexmaker-mockito = "2.28.3" jvm = "17" diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index 7eccfe5ed508..618dc37037aa 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -57,13 +57,13 @@ dependencies { 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.2.0-alpha12") + api("androidx.compose.material3:material3:1.2.0-beta02") 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.7.4") + api("androidx.navigation:navigation-compose:2.7.6") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.7.0-alpha03") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/SpaBaseDialogActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/SpaBaseDialogActivity.kt new file mode 100644 index 000000000000..dfb780af214b --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/SpaBaseDialogActivity.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.settingslib.spa + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import com.android.settingslib.spa.framework.common.LogCategory +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory +import com.android.settingslib.spa.framework.theme.SettingsTheme + +abstract class SpaBaseDialogActivity : ComponentActivity() { + private val spaEnvironment get() = SpaEnvironmentFactory.instance + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) + setContent { + SettingsTheme { + Content() + } + } + } + + @Composable + abstract fun Content() + + companion object { + private const val TAG = "SpaBaseDialogActivity" + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt index 5605485c4b84..da1ee77bcbfb 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt @@ -22,12 +22,14 @@ import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraph.Companion.findStartDestination @@ -133,6 +135,7 @@ private fun NavControllerWrapperImpl.NavContent( NavHost( navController = navController, startDestination = NullPageProvider.name, + modifier = Modifier.fillMaxSize(), ) { composable(NullPageProvider.name) {} for (spp in allProvider) { 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 81bee5ec0b94..0281ab817340 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 @@ -86,7 +86,7 @@ fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): Settings ) } -object NullPageProvider : SettingsPageProvider { +internal object NullPageProvider : SettingsPageProvider { override val name = NULL_PAGE_NAME } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt index 192b12500978..93ad644bf5de 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavGraphBuilder.kt @@ -30,6 +30,7 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.android.settingslib.spa.framework.common.NullPageProvider /** * Add the [Composable] to the [NavGraphBuilder] with animation @@ -49,11 +50,13 @@ internal fun NavGraphBuilder.animatedComposable( arguments = arguments, deepLinks = deepLinks, enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Start, - animationSpec = slideInEffect, - initialOffset = offsetFunc, - ) + fadeIn(animationSpec = fadeInEffect) + if (initialState.destination.route != NullPageProvider.name) { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = slideInEffect, + initialOffset = offsetFunc, + ) + fadeIn(animationSpec = fadeInEffect) + } else null }, exitTransition = { slideOutOfContainer( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt index 8ffd799385ba..de080e3d8ef4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt @@ -102,8 +102,8 @@ private fun AlertDialogPresenter.SettingsAlertDialog( fun getDialogWidth(): Dp { val configuration = LocalConfiguration.current return configuration.screenWidthDp.dp * when (configuration.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> 0.6f - else -> 0.8f + Configuration.ORIENTATION_LANDSCAPE -> 0.65f + else -> 0.85f } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt new file mode 100644 index 000000000000..1695e4f33915 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WarningAmber +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.window.DialogProperties + +@Composable +fun SettingsAlertDialogWithIcon( + onDismissRequest: () -> Unit, + confirmButton: AlertDialogButton?, + dismissButton: AlertDialogButton?, + title: String?, + text: @Composable (() -> Unit)?, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + icon = { Icon(Icons.Default.WarningAmber, contentDescription = null) }, + modifier = Modifier.width(getDialogWidth()), + confirmButton = { + confirmButton?.let { + Button( + onClick = { + it.onClick() + }, + ) { + Text(it.text) + } + } + }, + dismissButton = dismissButton?.let { + { + OutlinedButton( + onClick = { + it.onClick() + }, + ) { + Text(it.text) + } + } + }, + title = title?.let { + { + Text( + it, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + }, + text = text?.let { + { + Column(Modifier.verticalScroll(rememberScrollState())) { + text() + } + } + }, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) +}
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index abeffece2cd0..0a98791e8a6a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -24,6 +24,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ResolveInfo +import android.os.SystemProperties import com.android.internal.R import com.android.settingslib.spaprivileged.framework.common.userManager import kotlinx.coroutines.async @@ -110,7 +111,7 @@ class AppListRepositoryImpl( ): List<ApplicationInfo> { val disabledComponentsFlag = (PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() - val archivedPackagesFlag: Long = if (featureFlags.archiving()) + val archivedPackagesFlag: Long = if (isArchivingEnabled(featureFlags)) PackageManager.MATCH_ARCHIVED_PACKAGES else 0L val regularFlags = ApplicationInfoFlags.of( disabledComponentsFlag or @@ -148,6 +149,9 @@ class AppListRepositoryImpl( } } + private fun isArchivingEnabled(featureFlags: FeatureFlags) = + featureFlags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false) + override fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 1c830c1c5b06..74b556ea106e 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -162,6 +162,7 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp uid = checkNotNull(applicationInfo).uid, packageName = packageName) }) RestrictedSwitchPreference(switchModel, restrictions, restrictionsProviderFactory) + InfoPageAdditionalContent(record, isAllowed) } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt index 916d83af3f8f..3f7a8526839f 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt @@ -77,6 +77,9 @@ interface TogglePermissionAppListModel<T : AppRecord> { * Sets whether the permission is allowed for the given app. */ fun setAllowed(record: T, newAllowed: Boolean) + + @Composable + fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?){} } interface TogglePermissionAppListProvider { diff --git a/packages/SettingsLib/Tile/Android.bp b/packages/SettingsLib/Tile/Android.bp index 19c59dd221c2..54b97487c3cd 100644 --- a/packages/SettingsLib/Tile/Android.bp +++ b/packages/SettingsLib/Tile/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibTile", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index 77b7ac1246bd..e70201b0feb7 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibTopIntroPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/TwoTargetPreference/Android.bp b/packages/SettingsLib/TwoTargetPreference/Android.bp index 5aa906ec0ab3..70d9630d1865 100644 --- a/packages/SettingsLib/TwoTargetPreference/Android.bp +++ b/packages/SettingsLib/TwoTargetPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibTwoTargetPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/UsageProgressBarPreference/Android.bp b/packages/SettingsLib/UsageProgressBarPreference/Android.bp index 4cc90ccbfe80..0a83aabfce7b 100644 --- a/packages/SettingsLib/UsageProgressBarPreference/Android.bp +++ b/packages/SettingsLib/UsageProgressBarPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibUsageProgressBarPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/Utils/Android.bp b/packages/SettingsLib/Utils/Android.bp index d5a56c86431f..5d2aef780385 100644 --- a/packages/SettingsLib/Utils/Android.bp +++ b/packages/SettingsLib/Utils/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp index 390c9d2e98de..1f8d1dde15df 100644 --- a/packages/SettingsLib/search/Android.bp +++ b/packages/SettingsLib/search/Android.bp @@ -17,6 +17,9 @@ java_library { android_library { name: "SettingsLib-search", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], static_libs: [ "SettingsLib-search-interface", ], diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 071afaf18598..f7f06739d4b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -117,6 +117,12 @@ public class BluetoothUtils { } } + if (cachedDevice.isHearingAidDevice()) { + return new Pair<>(getBluetoothDrawable(context, + com.android.internal.R.drawable.ic_bt_hearing_aid), + context.getString(R.string.bluetooth_talkback_hearing_aids)); + } + List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); int resId = 0; for (LocalBluetoothProfile profile : profiles) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 032a8381a7b5..560bc467ca8e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -412,6 +412,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> public void setHearingAidInfo(HearingAidInfo hearingAidInfo) { mHearingAidInfo = hearingAidInfo; + dispatchAttributesChanged(); } public HearingAidInfo getHearingAidInfo() { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 7409eea2cd51..f7ec80b041e9 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -16,6 +16,7 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -87,6 +88,14 @@ public class BluetoothUtilsTest { } @Test + public void getBtClassDrawableWithDescription_typeHearingAid_returnHearingAidDrawable() { + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedBluetoothDevice); + + verify(mContext).getDrawable(com.android.internal.R.drawable.ic_bt_hearing_aid); + } + + @Test public void getBtRainbowDrawableWithDescription_normalHeadset_returnAdaptiveIcon() { when(mBluetoothDevice.getMetadata( BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn("false".getBytes()); diff --git a/packages/SettingsProvider/TEST_MAPPING b/packages/SettingsProvider/TEST_MAPPING index 890510ffebe3..0eed2b7490d4 100644 --- a/packages/SettingsProvider/TEST_MAPPING +++ b/packages/SettingsProvider/TEST_MAPPING @@ -11,5 +11,10 @@ } ] } + ], + "postsubmit": [ + { + "name": "CtsDeviceConfigTestCases" + } ] } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 5004f251cfd3..8ae50eb7ffad 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -241,6 +241,8 @@ public class SecureSettings { Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, + Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, + Settings.Secure.BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS, Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP, Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER, Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 0b0e182746e5..285c8c969343 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -387,6 +387,11 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_CODE, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, ANY_STRING_VALIDATOR); + VALIDATORS.put( + Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, + new DiscreteValueValidator(new String[] {"0", "1"})); + VALIDATORS.put( + Secure.BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index b0abf92ffe08..2d442f4c0e6e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -1349,6 +1349,26 @@ public class SettingsProvider extends ContentProvider { final int nameCount = names.size(); HashMap<String, String> flagsToValues = new HashMap<>(names.size()); + if (Flags.loadAconfigDefaults()) { + Map<String, Map<String, String>> allDefaults = + settingsState.getAconfigDefaultValues(); + + if (allDefaults != null) { + if (prefix != null) { + String namespace = prefix.substring(0, prefix.length() - 1); + + Map<String, String> namespaceDefaults = allDefaults.get(namespace); + if (namespaceDefaults != null) { + flagsToValues.putAll(namespaceDefaults); + } + } else { + for (Map<String, String> namespaceDefaults : allDefaults.values()) { + flagsToValues.putAll(namespaceDefaults); + } + } + } + } + for (int i = 0; i < nameCount; i++) { String name = names.get(i); Setting setting = settingsState.getSettingLocked(name); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 73c2e22240d3..6f3c88fc8706 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -69,6 +69,7 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -236,6 +237,10 @@ final class SettingsState { @GuardedBy("mLock") private int mNextHistoricalOpIdx; + @GuardedBy("mLock") + @Nullable + private Map<String, Map<String, String>> mNamespaceDefaults; + public static final int SETTINGS_TYPE_GLOBAL = 0; public static final int SETTINGS_TYPE_SYSTEM = 1; public static final int SETTINGS_TYPE_SECURE = 2; @@ -331,25 +336,21 @@ final class SettingsState { readStateSyncLocked(); if (Flags.loadAconfigDefaults()) { - // Only load aconfig defaults if this is the first boot, the XML - // file doesn't exist yet, or this device is on its first boot after - // an OTA. - boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey) - && (!file.exists() - || mContext.getPackageManager().isDeviceUpgrading()); - if (shouldLoadAconfigValues) { + if (isConfigSettingsKey(mKey)) { loadAconfigDefaultValuesLocked(); } } + } } @GuardedBy("mLock") private void loadAconfigDefaultValuesLocked() { + mNamespaceDefaults = new HashMap<>(); + for (String fileName : sAconfigTextProtoFilesOnDevice) { try (FileInputStream inputStream = new FileInputStream(fileName)) { - byte[] contents = inputStream.readAllBytes(); - loadAconfigDefaultValues(contents); + loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults); } catch (IOException e) { Slog.e(LOG_TAG, "failed to read protobuf", e); } @@ -358,27 +359,21 @@ final class SettingsState { @VisibleForTesting @GuardedBy("mLock") - public void loadAconfigDefaultValues(byte[] fileContents) { + public static void loadAconfigDefaultValues(byte[] fileContents, + @NonNull Map<String, Map<String, String>> defaultMap) { try { - parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); - - if (parsedFlags == null) { - Slog.e(LOG_TAG, "failed to parse aconfig protobuf"); - return; - } - + parsed_flags parsedFlags = + parsed_flags.parseFrom(fileContents); for (parsed_flag flag : parsedFlags.getParsedFlagList()) { - String flagName = flag.getNamespace() + "/" - + flag.getPackage() + "." + flag.getName(); - String value = flag.getState() == flag_state.ENABLED ? "true" : "false"; - - Setting existingSetting = getSettingLocked(flagName); - boolean isDefaultLoaded = existingSetting.getTag() != null - && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG); - if (existingSetting.getValue() == null || isDefaultLoaded) { - insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG, - false, flag.getPackage()); + if (!defaultMap.containsKey(flag.getNamespace())) { + Map<String, String> defaults = new HashMap<>(); + defaultMap.put(flag.getNamespace(), defaults); } + String flagName = flag.getNamespace() + + "/" + flag.getPackage() + "." + flag.getName(); + String flagValue = flag.getState() == flag_state.ENABLED + ? "true" : "false"; + defaultMap.get(flag.getNamespace()).put(flagName, flagValue); } } catch (IOException e) { Slog.e(LOG_TAG, "failed to parse protobuf", e); @@ -443,6 +438,13 @@ final class SettingsState { return names; } + @Nullable + public Map<String, Map<String, String>> getAconfigDefaultValues() { + synchronized (mLock) { + return mNamespaceDefaults; + } + } + // The settings provider must hold its lock when calling here. public Setting getSettingLocked(String name) { if (TextUtils.isEmpty(name)) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index ecac5ee18582..edbc0b391b27 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -14,3 +14,11 @@ flag { bug: "311155098" is_fixed_read_only: true } + +flag { + name: "configurable_font_scale_default" + namespace: "large_screen_experiences_app_compat" + description: "Whether the font_scale is read from a device dependent configuration file" + bug: "319808237" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 24625eaa5e13..e55bbecb67d7 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -30,6 +30,8 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; public class SettingsStateTest extends AndroidTestCase { public static final String CRAZY_STRING = @@ -93,7 +95,6 @@ public class SettingsStateTest extends AndroidTestCase { SettingsState settingsState = new SettingsState( getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - parsed_flags flags = parsed_flags .newBuilder() .addParsedFlag(parsed_flag @@ -117,18 +118,13 @@ public class SettingsStateTest extends AndroidTestCase { .build(); synchronized (lock) { - settingsState.loadAconfigDefaultValues(flags.toByteArray()); - settingsState.persistSettingsLocked(); - } - settingsState.waitForHandler(); + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); + Map<String, String> namespaceDefaults = defaults.get("test_namespace"); + assertEquals(2, namespaceDefaults.keySet().size()); - synchronized (lock) { - assertEquals("false", - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag1").getValue()); - assertEquals("true", - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag2").getValue()); + assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1")); + assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2")); } } @@ -150,21 +146,18 @@ public class SettingsStateTest extends AndroidTestCase { .build(); synchronized (lock) { - settingsState.loadAconfigDefaultValues(flags.toByteArray()); - settingsState.persistSettingsLocked(); - } - settingsState.waitForHandler(); + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); - synchronized (lock) { - assertEquals(null, - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag1").getValue()); + Map<String, String> namespaceDefaults = defaults.get("test_namespace"); + assertEquals(null, namespaceDefaults); } } public void testInvalidAconfigProtoDoesNotCrash() { + Map<String, Map<String, String>> defaults = new HashMap<>(); SettingsState settingsState = getSettingStateObject(); - settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes()); + settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults); } public void testIsBinary() { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 507d9c467d68..db1ca95fd83e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -559,6 +559,9 @@ <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.TEST_BIOMETRIC" /> + <!-- Permission required for CTS test - android.server.biometrics --> + <uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <!-- Permissions required for CTS test - NotificationManagerTest --> <uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" /> @@ -748,6 +751,7 @@ <uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" /> <uses-permission android:name="android.permission.MODIFY_CELL_BROADCASTS" /> <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" /> + <uses-permission android:name="android.permission.ACCESS_LAST_KNOWN_CELL_ID" /> <!-- Permission required for CTS test - CtsPersistentDataBlockManagerTestCases --> <uses-permission android:name="android.permission.ACCESS_PDB_STATE" /> @@ -891,6 +895,9 @@ <!-- Permission required for Cts test - CtsNotificationTestCases --> <uses-permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" /> + <!-- Permission required for BinaryTransparencyService shell API and host test --> + <uses-permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 7443e4ccf79e..168e6e003dc6 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -355,6 +355,8 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> + <uses-permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" /> + <!-- Listen to (dis-)connection of external displays and enable / disable them. --> <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index f7b1a26c9df9..7ba889bc8fee 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -36,3 +36,10 @@ flag { description: "Animates the floating menu's transition between curved and jagged edges." bug: "281140482" } + +flag { + name: "create_windowless_window_magnifier" + namespace: "accessibility" + description: "Uses SurfaceControlViewHost to create the magnifier for window magnification." + bug: "280992417" +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index c23a49c68363..323613077a70 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -15,6 +15,13 @@ flag { } flag { + name: "notification_async_group_header_inflation" + namespace: "systemui" + description: "Inflates the notification group summary header views from the background thread." + bug: "217799515" +} + +flag { name: "notification_async_hybrid_view_inflation" namespace: "systemui" description: "Inflates hybrid (single-line) notification views from the background thread." @@ -343,3 +350,10 @@ flag { description: "Relocate Smartspace to bottom of the Lock Screen" bug: "316212788" } + +flag { + name: "pin_input_field_styled_focus_state" + namespace: "systemui" + description: "Enables styled focus states on pin input field if keyboard is connected" + bug: "316106516" +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index b70486473672..c073b79ba5a3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -67,8 +67,9 @@ fun CommunalContainer( // Don't show hub mode UI if keyguard is not present. This is important since we're in the // shade, which can be opened from many locations. val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false) + val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState() - if (!isKeyguardShowing) { + if (!isKeyguardShowing || !isCommunalAvailable) { return } 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 91a4d2e01e90..a390305b144e 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 @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -38,6 +39,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape @@ -58,7 +60,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -67,8 +71,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow @@ -83,9 +89,14 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup +import androidx.core.view.setPadding import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.ui.compose.Dimensions.CardOutlineWidth +import com.android.systemui.communal.ui.compose.extensions.allowGestures +import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset +import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.res.R @@ -104,22 +115,59 @@ fun CommunalHub( var toolbarSize: IntSize? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var isDraggingToRemove by remember { mutableStateOf(false) } + val gridState = rememberLazyGridState() + val contentListState = rememberContentListState(communalContent, viewModel) + val reorderingWidgets by viewModel.reorderingWidgets.collectAsState() + val selectedIndex = viewModel.selectedIndex.collectAsState() + val removeButtonEnabled by remember { + derivedStateOf { selectedIndex.value != null || reorderingWidgets } + } + + val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) + val contentOffset = beforeContentPadding(contentPadding).toOffset() Box( modifier = - modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant), + modifier + .fillMaxSize() + .background(LocalAndroidColorScheme.current.outlineVariant) + .pointerInput(gridState, contentOffset, contentListState) { + // If not in edit mode, don't allow selecting items. + if (!viewModel.isEditMode) return@pointerInput + observeTapsWithoutConsuming { offset -> + val adjustedOffset = offset - contentOffset + val index = + gridState.layoutInfo.visibleItemsInfo + .firstItemAtOffset(adjustedOffset) + ?.index + val newIndex = + if (index?.let(contentListState::isItemEditable) == true) { + index + } else { + null + } + viewModel.setSelectedIndex(newIndex) + } + }, ) { CommunalHubLazyGrid( communalContent = communalContent, viewModel = viewModel, - contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize), + contentPadding = contentPadding, + contentOffset = contentOffset, setGridCoordinates = { gridCoordinates = it }, - updateDragPositionForRemove = { + updateDragPositionForRemove = { offset -> isDraggingToRemove = - checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates) + isPointerWithinCoordinates( + offset = gridCoordinates?.let { it.positionInWindow() + offset }, + containerToCheck = removeButtonCoordinates + ) isDraggingToRemove }, onOpenWidgetPicker = onOpenWidgetPicker, + gridState = gridState, + contentListState = contentListState, + selectedIndex = selectedIndex ) if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) { @@ -129,6 +177,14 @@ fun CommunalHub( setRemoveButtonCoordinates = { removeButtonCoordinates = it }, onEditDone = onEditDone, onOpenWidgetPicker = onOpenWidgetPicker, + onRemoveClicked = { + selectedIndex.value?.let { index -> + contentListState.onRemove(index) + contentListState.onSaveList() + viewModel.setSelectedIndex(null) + } + }, + removeEnabled = removeButtonEnabled ) } else { IconButton(onClick = viewModel::onOpenWidgetEditor) { @@ -158,16 +214,18 @@ private fun BoxScope.CommunalHubLazyGrid( communalContent: List<CommunalContentModel>, viewModel: BaseCommunalViewModel, contentPadding: PaddingValues, + selectedIndex: State<Int?>, + contentOffset: Offset, + gridState: LazyGridState, + contentListState: ContentListState, setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit, updateDragPositionForRemove: (offset: Offset) -> Boolean, onOpenWidgetPicker: (() -> Unit)? = null, ) { var gridModifier = Modifier.align(Alignment.CenterStart) - val gridState = rememberLazyGridState() var list = communalContent var dragDropState: GridDragDropState? = null if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) { - val contentListState = rememberContentListState(list, viewModel) list = contentListState.list // for drag & drop operations within the communal hub grid dragDropState = @@ -179,7 +237,7 @@ private fun BoxScope.CommunalHubLazyGrid( gridModifier = gridModifier .fillMaxSize() - .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel) + .dragContainer(dragDropState, contentOffset, viewModel) .onGloballyPositioned { setGridCoordinates(it) } // for widgets dropped from other activities val dragAndDropTargetState = @@ -218,8 +276,10 @@ private fun BoxScope.CommunalHubLazyGrid( list[index].size.dp().value, ) if (viewModel.isEditMode && dragDropState != null) { + val selected by remember(index) { derivedStateOf { index == selectedIndex.value } } DraggableItem( dragDropState = dragDropState, + selected = selected, enabled = list[index] is CommunalContentModel.Widget, index = index, size = size @@ -253,11 +313,19 @@ private fun BoxScope.CommunalHubLazyGrid( @Composable private fun Toolbar( isDraggingToRemove: Boolean, + removeEnabled: Boolean, + onRemoveClicked: () -> Unit, setToolbarSize: (toolbarSize: IntSize) -> Unit, setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit, onOpenWidgetPicker: () -> Unit, - onEditDone: () -> Unit, + onEditDone: () -> Unit ) { + val removeButtonAlpha: Float by + animateFloatAsState( + targetValue = if (removeEnabled) 1f else 0.5f, + label = "RemoveButtonAlphaAnimation" + ) + Row( modifier = Modifier.fillMaxWidth() @@ -301,13 +369,18 @@ private fun Toolbar( } } else { OutlinedButton( - // Button is disabled to make it non-clickable - enabled = false, - onClick = {}, - colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary), + enabled = removeEnabled, + onClick = onRemoveClicked, + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = colors.primary, + disabledContentColor = colors.primary + ), border = BorderStroke(width = 1.0.dp, color = colors.primary), contentPadding = Dimensions.ButtonPadding, - modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } + modifier = + Modifier.graphicsLayer { alpha = removeButtonAlpha } + .onGloballyPositioned { setRemoveButtonCoordinates(it) } ) { RemoveButtonContent(spacerModifier) } @@ -385,7 +458,7 @@ private fun CommunalContent( ) { when (model) { is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier) - is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size) + is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size) is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, size, modifier) is CommunalContentModel.CtaTileInEditMode -> @@ -396,13 +469,13 @@ private fun CommunalContent( } } -/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */ +/** Creates an empty card used to highlight a particular spot on the grid. */ @Composable -fun WidgetPlaceholderContent(size: SizeF) { +fun HighlightedItem(size: SizeF, modifier: Modifier = Modifier) { Card( - modifier = Modifier.size(Dp(size.width), Dp(size.height)), + modifier = modifier.size(Dp(size.width), Dp(size.height)), colors = CardDefaults.cardColors(containerColor = Color.Transparent), - border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed), + border = BorderStroke(CardOutlineWidth, LocalAndroidColorScheme.current.tertiaryFixed), shape = RoundedCornerShape(16.dp) ) {} } @@ -416,7 +489,7 @@ private fun CtaTileInViewModeContent( ) { val colors = LocalAndroidColorScheme.current Card( - modifier = modifier.height(size.height.dp), + modifier = modifier.height(size.height.dp).padding(CardOutlineWidth), colors = CardDefaults.cardColors( containerColor = colors.primary, @@ -488,7 +561,7 @@ private fun CtaTileInEditModeContent( } val colors = LocalAndroidColorScheme.current Card( - modifier = modifier.height(size.height.dp), + modifier = modifier.height(size.height.dp).padding(CardOutlineWidth), colors = CardDefaults.cardColors(containerColor = Color.Transparent), border = BorderStroke(1.dp, colors.primary), shape = RoundedCornerShape(200.dp), @@ -527,8 +600,9 @@ private fun WidgetContent( modifier = modifier.height(size.height.dp), contentAlignment = Alignment.Center, ) { + val paddingInPx = with(LocalDensity.current) { CardOutlineWidth.toPx().toInt() } AndroidView( - modifier = modifier, + modifier = modifier.allowGestures(allowed = !viewModel.isEditMode), factory = { context -> // The AppWidgetHostView will inherit the interaction handler from the // AppWidgetHost. So set the interaction handler here before creating the view, and @@ -538,9 +612,13 @@ private fun WidgetContent( model.appWidgetHost.setInteractionHandler(viewModel.getInteractionHandler()) val view = model.appWidgetHost - .createView(context, model.appWidgetId, model.providerInfo) + .createViewForCommunal(context, model.appWidgetId, model.providerInfo) .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) } model.appWidgetHost.setInteractionHandler(null) + // Remove the extra padding applied to AppWidgetHostView to allow widgets to + // occupy the entire box. The added padding is now adjusted to leave only sufficient + // space for displaying the outline around the box when the widget is selected. + view.setPadding(paddingInPx) view }, // For reusing composition in lazy lists. @@ -616,8 +694,8 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx { return with(LocalDensity.current) { ContentPaddingInPx( - startPadding = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), - topPadding = paddingValues.calculateTopPadding().toPx() + start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), + top = paddingValues.calculateTopPadding().toPx() ) } } @@ -626,18 +704,15 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn * Check whether the pointer position that the item is being dragged at is within the coordinates of * the remove button in the toolbar. Returns true if the item is removable. */ -private fun checkForDraggingToRemove( - offset: Offset, - removeButtonCoordinates: LayoutCoordinates?, - gridCoordinates: LayoutCoordinates?, +private fun isPointerWithinCoordinates( + offset: Offset?, + containerToCheck: LayoutCoordinates? ): Boolean { - if (removeButtonCoordinates == null || gridCoordinates == null) { + if (offset == null || containerToCheck == null) { return false } - val pointer = gridCoordinates.positionInWindow() + offset - val removeButton = removeButtonCoordinates.positionInWindow() - return pointer.x in removeButton.x..removeButton.x + removeButtonCoordinates.size.width && - pointer.y in removeButton.y..removeButton.y + removeButtonCoordinates.size.height + val container = containerToCheck.boundsInWindow() + return container.contains(offset) } private fun CommunalContentSize.dp(): Dp { @@ -648,13 +723,16 @@ private fun CommunalContentSize.dp(): Dp { } } -data class ContentPaddingInPx(val startPadding: Float, val topPadding: Float) +data class ContentPaddingInPx(val start: Float, val top: Float) { + fun toOffset(): Offset = Offset(start, top) +} object Dimensions { val CardWidth = 464.dp val CardHeightFull = 630.dp val CardHeightHalf = 307.dp val CardHeightThird = 199.dp + val CardOutlineWidth = 3.dp val GridHeight = CardHeightFull val Spacing = 16.dp diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 979991d7dc2a..45f98b879dd7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -21,12 +21,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateList import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel @Composable fun rememberContentListState( communalContent: List<CommunalContentModel>, - viewModel: CommunalEditModeViewModel, + viewModel: BaseCommunalViewModel, ): ContentListState { return remember(communalContent) { ContentListState( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index 113822167ca7..a1959532fbb9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -17,6 +17,10 @@ package com.android.systemui.communal.ui.compose import android.util.SizeF +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy @@ -32,6 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput @@ -39,6 +44,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex +import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.ui.compose.extensions.plus import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import kotlinx.coroutines.CoroutineScope @@ -109,13 +115,10 @@ internal constructor( internal fun onDragStart(offset: Offset, contentOffset: Offset) { state.layoutInfo.visibleItemsInfo - .firstOrNull { item -> - // grid item offset is based off grid content container so we need to deduct - // before content padding from the initial pointer position - contentListState.isItemEditable(item.index) && - (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x && - (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y - } + .filter { item -> contentListState.isItemEditable(item.index) } + // grid item offset is based off grid content container so we need to deduct + // before content padding from the initial pointer position + .firstItemAtOffset(offset - contentOffset) ?.apply { dragStartPointerOffset = offset - this.offset.toOffset() draggingItemIndex = index @@ -148,12 +151,11 @@ internal constructor( val middleOffset = startOffset + (endOffset - startOffset) / 2f val targetItem = - state.layoutInfo.visibleItemsInfo.find { item -> - contentListState.isItemEditable(item.index) && - middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x && - middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y && - draggingItem.index != item.index - } + state.layoutInfo.visibleItemsInfo + .asSequence() + .filter { item -> contentListState.isItemEditable(item.index) } + .filter { item -> draggingItem.index != item.index } + .firstItemAtOffset(middleOffset) if (targetItem != null) { val scrollToIndex = @@ -208,32 +210,31 @@ internal constructor( fun Modifier.dragContainer( dragDropState: GridDragDropState, - beforeContentPadding: ContentPaddingInPx, + contentOffset: Offset, viewModel: BaseCommunalViewModel, ): Modifier { - return pointerInput(dragDropState, beforeContentPadding) { - detectDragGesturesAfterLongPress( - onDrag = { change, offset -> - change.consume() - dragDropState.onDrag(offset = offset) - }, - onDragStart = { offset -> - dragDropState.onDragStart( - offset, - Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding) - ) - viewModel.onReorderWidgetStart() - }, - onDragEnd = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetEnd() - }, - onDragCancel = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetCancel() - } - ) - } + return this.then( + pointerInput(dragDropState, contentOffset) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> + dragDropState.onDragStart(offset, contentOffset) + viewModel.onReorderWidgetStart() + }, + onDragEnd = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetEnd() + }, + onDragCancel = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetCancel() + } + ) + } + ) } /** Wrap LazyGrid item with additional modifier needed for drag and drop. */ @@ -243,6 +244,7 @@ fun LazyGridItemScope.DraggableItem( dragDropState: GridDragDropState, index: Int, enabled: Boolean, + selected: Boolean, size: SizeF, modifier: Modifier = Modifier, content: @Composable (isDragging: Boolean) -> Unit @@ -250,21 +252,31 @@ fun LazyGridItemScope.DraggableItem( if (!enabled) { return Box(modifier = modifier) { content(false) } } + val dragging = index == dragDropState.draggingItemIndex + val itemAlpha: Float by + animateFloatAsState( + targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f, + label = "DraggableItemAlpha" + ) val draggingModifier = if (dragging) { Modifier.zIndex(1f).graphicsLayer { translationX = dragDropState.draggingItemOffset.x translationY = dragDropState.draggingItemOffset.y - alpha = if (dragDropState.isDraggingToRemove) 0.5f else 1f + alpha = itemAlpha } } else { Modifier.animateItemPlacement() } Box(modifier) { - if (dragging) { - WidgetPlaceholderContent(size) + AnimatedVisibility( + visible = (dragging || selected) && !dragDropState.isDraggingToRemove, + enter = fadeIn(), + exit = fadeOut() + ) { + HighlightedItem(size) } Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt new file mode 100644 index 000000000000..132093f034bb --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.toRect + +/** + * Determine the item at the specified offset, or null if none exist. + * + * @param offset The offset in pixels, relative to the top start of the grid. + */ +fun Iterable<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? = + firstOrNull { item -> + isItemAtOffset(item, offset) + } + +/** + * Determine the item at the specified offset, or null if none exist. + * + * @param offset The offset in pixels, relative to the top start of the grid. + */ +fun Sequence<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? = + firstOrNull { item -> + isItemAtOffset(item, offset) + } + +private fun isItemAtOffset(item: LazyGridItemInfo, offset: Offset): Boolean { + val boundingBox = IntRect(item.offset, item.size) + return boundingBox.toRect().contains(offset) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt new file mode 100644 index 000000000000..b31008e04593 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput + +/** Sets whether gestures are allowed on children of this element. */ +fun Modifier.allowGestures(allowed: Boolean): Modifier = + if (allowed) { + this + } else { + this.then(pointerInput(Unit) { consumeAllGestures() }) + } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt new file mode 100644 index 000000000000..14074944259b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import kotlinx.coroutines.coroutineScope + +/** + * Observe taps without actually consuming them, so child elements can still respond to them. Long + * presses are excluded. + */ +suspend fun PointerInputScope.observeTapsWithoutConsuming( + pass: PointerEventPass = PointerEventPass.Initial, + onTap: ((Offset) -> Unit)? = null, +) = coroutineScope { + if (onTap == null) return@coroutineScope + awaitEachGesture { + awaitFirstDown(pass = pass) + val tapTimeout = viewConfiguration.longPressTimeoutMillis + val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) } + if (up != null) { + onTap(up.position) + } + } +} + +/** Consume all gestures on the initial pass so that child elements do not receive them. */ +suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { + awaitEachGesture { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .forEach(PointerInputChange::consume) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 900616f6af89..42fcd1363f11 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -16,23 +16,55 @@ package com.android.systemui.keyguard.ui.composable.section +import android.content.Context +import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.notifications.ui.composable.NotificationStack +import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject class NotificationSection @Inject constructor( + @Application context: Context, private val viewModel: NotificationsPlaceholderViewModel, + controller: NotificationStackScrollLayoutController, + sceneContainerFlags: SceneContainerFlags, + sharedNotificationContainer: SharedNotificationContainer, + stackScrollLayout: NotificationStackScrollLayout, + notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, + ambientState: AmbientState, ) { + init { + if (sceneContainerFlags.flexiNotifsEnabled()) { + (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout) + sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) + + NotificationStackAppearanceViewBinder.bind( + context, + sharedNotificationContainer, + notificationStackAppearanceViewModel, + ambientState, + controller, + ) + } + } + @Composable fun SceneScope.Notifications(modifier: Modifier = Modifier) { NotificationStack( viewModel = viewModel, - isScrimVisible = false, modifier = modifier, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 0b26ae96de54..e835d3e576d5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -43,8 +44,10 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.height import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import kotlin.math.roundToInt object Notifications { object Elements { @@ -77,32 +80,52 @@ fun SceneScope.HeadsUpNotificationSpace( ) } -/** Adds the space where notification stack will appear in the scene. */ +/** Adds the space where notification stack should appear in the scene. */ @Composable fun SceneScope.NotificationStack( viewModel: NotificationsPlaceholderViewModel, - isScrimVisible: Boolean, + modifier: Modifier = Modifier, +) { + NotificationPlaceholder( + viewModel = viewModel, + form = Form.Stack, + modifier = modifier, + ) +} + +/** + * Adds the space where notification stack should appear in the scene, with a scrim and nested + * scrolling. + */ +@Composable +fun SceneScope.NotificationScrollingStack( + viewModel: NotificationsPlaceholderViewModel, modifier: Modifier = Modifier, ) { val cornerRadius by viewModel.cornerRadiusDp.collectAsState() - Box(modifier = modifier) { - if (isScrimVisible) { - Box( - modifier = - Modifier.element(Notifications.Elements.NotificationScrim) - .fillMaxSize() - .graphicsLayer { - shape = RoundedCornerShape(cornerRadius.dp) - clip = true - } - .background(MaterialTheme.colorScheme.surface) - ) - } + val contentHeight by viewModel.intrinsicContentHeight.collectAsState() + + val expansionFraction by viewModel.expandFraction.collectAsState(0f) + + Box( + modifier = + modifier + .verticalNestedScrollToScene() + .fillMaxWidth() + .element(Notifications.Elements.NotificationScrim) + .graphicsLayer { + shape = RoundedCornerShape(cornerRadius.dp) + clip = true + alpha = expansionFraction + } + .background(MaterialTheme.colorScheme.surface) + .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f)) + ) { NotificationPlaceholder( viewModel = viewModel, form = Form.Stack, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() } ) } } @@ -159,6 +182,7 @@ private fun SceneScope.NotificationPlaceholder( debugLog(viewModel) { "STACK onSizeChanged: size=$size" } } .onPlaced { coordinates: LayoutCoordinates -> + viewModel.onContentTopChanged(coordinates.positionInWindow().y) debugLog(viewModel) { "STACK onPlaced:" + " size=${coordinates.size}" + diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index bded98d52481..747faabe514b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace +import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.Edge import com.android.systemui.scene.shared.model.SceneKey @@ -66,6 +67,7 @@ constructor( modifier: Modifier, ) { Box(modifier = modifier) { + Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim)) HeadsUpNotificationSpace( viewModel = notificationsViewModel, modifier = Modifier.padding(16.dp).fillMaxSize(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt index 6bb525aa00fb..0c2c5195becc 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt @@ -3,12 +3,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder -import com.android.systemui.notifications.ui.composable.Notifications -import com.android.systemui.scene.ui.composable.Shade +import com.android.systemui.qs.ui.composable.QuickSettings +import com.android.systemui.shade.ui.composable.ShadeHeader fun TransitionBuilder.goneToShadeTransition() { spec = tween(durationMillis = 500) - translate(Shade.rootElementKey, Edge.Top, true) - fade(Notifications.Elements.NotificationScrim) + fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContent) } + translate(QuickSettings.Elements.Content, Edge.Top, true) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 9c0f1fe0ec68..1545372686c9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -22,11 +22,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -44,10 +42,12 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.controls.ui.MediaCarouselController +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL -import com.android.systemui.notifications.ui.composable.NotificationStack +import com.android.systemui.notifications.ui.composable.NotificationScrollingStack import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Direction @@ -128,6 +128,12 @@ constructor( modifier = modifier, ) + init { + mediaHost.expansion = MediaHostState.EXPANDED + mediaHost.showsOnlyActiveMedia = true + mediaHost.init(MediaHierarchyManager.LOCATION_QQS) + } + private fun destinationScenes( up: SceneKey, ): Map<UserAction, SceneModel> { @@ -148,35 +154,27 @@ private fun SceneScope.ShadeScene( mediaHost: MediaHost, modifier: Modifier = Modifier, ) { + val localDensity = LocalDensity.current val layoutWidth = remember { mutableStateOf(0) } - Box(modifier.element(Shade.Elements.Scrim)) { - Spacer( - modifier = - Modifier.element(Shade.Elements.ScrimBackground) - .fillMaxSize() - .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) - ) + Box( + modifier = + modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim), + ) + Box { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier.fillMaxSize() - .clickable(onClick = { viewModel.onContentClicked() }) - .padding( - start = Shade.Dimensions.HorizontalPadding, - end = Shade.Dimensions.HorizontalPadding, - bottom = 48.dp - ) + modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() }) ) { CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) ) - Spacer(modifier = Modifier.height(16.dp)) QuickSettings( - modifier = Modifier.wrapContentHeight(), + modifier = Modifier.height(130.dp), viewModel.qsSceneAdapter, ) @@ -202,16 +200,15 @@ private fun SceneScope.ShadeScene( }, mediaHost = mediaHost, layoutWidth = layoutWidth.value, - layoutHeight = with(LocalDensity.current) { mediaHeight.toPx() }.toInt(), + layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(), carouselController = mediaCarouselController, ) } Spacer(modifier = Modifier.height(16.dp)) - NotificationStack( + NotificationScrollingStack( viewModel = viewModel.notifications, - isScrimVisible = true, - modifier = Modifier.weight(1f), + modifier = Modifier.fillMaxWidth().weight(1f), ) } } diff --git a/packages/SystemUI/compose/features/tests/AndroidManifest.xml b/packages/SystemUI/compose/features/tests/AndroidManifest.xml index 8fe9656c1879..fc337fb19f43 100644 --- a/packages/SystemUI/compose/features/tests/AndroidManifest.xml +++ b/packages/SystemUI/compose/features/tests/AndroidManifest.xml @@ -30,11 +30,6 @@ android:enabled="false" tools:replace="android:authorities" tools:node="remove" /> - <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle" - android:authorities="com.android.systemui.test.keyguard.disabled" - android:enabled="false" - tools:replace="android:authorities" - tools:node="remove" /> <provider android:name="com.android.systemui.keyguard.CustomizationProvider" android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled" android:enabled="false" diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt index 2c96d0e5402a..8e35988832dc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt @@ -36,24 +36,25 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection fun LargeTopAppBarNestedScrollConnection( height: () -> Float, onHeightChanged: (Float) -> Unit, - heightRange: ClosedFloatingPointRange<Float>, + minHeight: () -> Float, + maxHeight: () -> Float, ): PriorityNestedScrollConnection { - val minHeight = heightRange.start - val maxHeight = heightRange.endInclusive return PriorityNestedScrollConnection( orientation = Orientation.Vertical, // When swiping up, the LargeTopAppBar will shrink (to [minHeight]) and the content will // expand. Then, you can then scroll down the content. canStartPreScroll = { offsetAvailable, offsetBeforeStart -> - offsetAvailable < 0 && offsetBeforeStart == 0f && height() > minHeight + offsetAvailable < 0 && offsetBeforeStart == 0f && height() > minHeight() }, // When swiping down, the content will scroll up until it reaches the top. Then, the // LargeTopAppBar will expand until it reaches its [maxHeight]. - canStartPostScroll = { offsetAvailable, _ -> offsetAvailable > 0 && height() < maxHeight }, + canStartPostScroll = { offsetAvailable, _ -> + offsetAvailable > 0 && height() < maxHeight() + }, canStartPostFling = { false }, canContinueScroll = { val currentHeight = height() - minHeight < currentHeight && currentHeight < maxHeight + minHeight() < currentHeight && currentHeight < maxHeight() }, canScrollOnFling = true, onStart = { /* do nothing */}, @@ -61,10 +62,10 @@ fun LargeTopAppBarNestedScrollConnection( val currentHeight = height() val amountConsumed = if (offsetAvailable > 0) { - val amountLeft = maxHeight - currentHeight + val amountLeft = maxHeight() - currentHeight offsetAvailable.coerceAtMost(amountLeft) } else { - val amountLeft = minHeight - currentHeight + val amountLeft = minHeight() - currentHeight offsetAvailable.coerceAtLeast(amountLeft) } onHeightChanged(currentHeight + amountConsumed) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt index e2974cddf1b9..ac7717b41a5a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt @@ -34,7 +34,8 @@ class LargeTopAppBarNestedScrollConnectionTest(testCase: TestCase) { LargeTopAppBarNestedScrollConnection( height = { height }, onHeightChanged = { height = it }, - heightRange = heightRange, + minHeight = { heightRange.start }, + maxHeight = { heightRange.endInclusive }, ) private fun NestedScrollConnection.scroll( diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index e89316997fb2..c4bcb536de78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -35,6 +35,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -107,7 +108,7 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener, mEmergencyButtonController, mFalsingCollector, featureFlags, - mSelectedUserInteractor) { + mSelectedUserInteractor, new FakeKeyboardRepository()) { @Override public void onResume(int reason) { super.onResume(reason); diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 78b854e39d13..c2efc05132ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.res.R import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED @@ -141,7 +142,8 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { postureController, featureFlags, mSelectedUserInteractor, - uiEventLogger + uiEventLogger, + FakeKeyboardRepository() ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index bc3ca1bd6c56..2a793ea70292 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -47,16 +47,20 @@ import com.android.systemui.classifier.FalsingA11yDelegate import com.android.systemui.classifier.FalsingCollector import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.log.SessionTracker import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -66,6 +70,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any @@ -156,7 +161,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController private lateinit var keyguardPasswordView: KeyguardPasswordView private lateinit var testableResources: TestableResources - private lateinit var sceneTestUtils: SceneTestUtils + private lateinit var kosmos: Kosmos private lateinit var sceneInteractor: SceneInteractor private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private lateinit var deviceEntryInteractor: DeviceEntryInteractor @@ -222,15 +227,15 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mSelectedUserInteractor, ) - sceneTestUtils = SceneTestUtils(this) - sceneInteractor = sceneTestUtils.sceneInteractor() + kosmos = testKosmos() + sceneInteractor = kosmos.sceneInteractor keyguardTransitionInteractor = - KeyguardTransitionInteractorFactory.create(sceneTestUtils.testScope.backgroundScope) + KeyguardTransitionInteractorFactory.create(kosmos.testScope.backgroundScope) .keyguardTransitionInteractor sceneTransitionStateFlow = MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen)) sceneInteractor.setTransitionState(sceneTransitionStateFlow) - deviceEntryInteractor = sceneTestUtils.deviceEntryInteractor() + deviceEntryInteractor = kosmos.deviceEntryInteractor mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR) underTest = @@ -249,7 +254,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { falsingManager, userSwitcherController, featureFlags, - sceneTestUtils.fakeSceneContainerFlags, + kosmos.fakeSceneContainerFlags, globalSettings, sessionTracker, Optional.of(sideFpsController), @@ -259,7 +264,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { audioManager, faceAuthInteractor, mock(), - { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, + { JavaAdapter(kosmos.testScope.backgroundScope) }, mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, @@ -786,8 +791,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Test fun dismissesKeyguard_whenSceneChangesToGone() = - sceneTestUtils.testScope.runTest { - sceneTestUtils.fakeSceneContainerFlags.enabled = true + kosmos.testScope.runTest { + kosmos.fakeSceneContainerFlags.enabled = true // Upon init, we have never dismisses the keyguard. underTest.onInit() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index f7751753cc18..0959f1b2bcf6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -16,7 +16,6 @@ package com.android.keyguard -import android.telephony.PinResult import android.telephony.TelephonyManager import android.testing.TestableLooper import android.view.LayoutInflater @@ -28,9 +27,11 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -39,7 +40,6 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -75,8 +75,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { `when`(messageAreaControllerFactory.create(Mockito.any(KeyguardMessageArea::class.java))) .thenReturn(keyguardMessageAreaController) `when`(telephonyManager.createForSubscriptionId(anyInt())).thenReturn(telephonyManager) - `when`(telephonyManager.supplyIccLockPin(anyString())) - .thenReturn(mock(PinResult::class.java)) + `when`(telephonyManager.supplyIccLockPin(anyString())).thenReturn(mock()) simPinView = LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null) as KeyguardSimPinView @@ -97,7 +96,8 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { falsingCollector, emergencyButtonController, fakeFeatureFlags, - mSelectedUserInteractor + mSelectedUserInteractor, + FakeKeyboardRepository() ) underTest.init() underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 45a60199984b..1281e4409a83 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any @@ -91,6 +92,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { emergencyButtonController, fakeFeatureFlags, mSelectedUserInteractor, + FakeKeyboardRepository() ) underTest.init() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index 27d1eb741bb0..c86c7470909b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -17,14 +17,15 @@ package com.android.systemui.accessibility.data.repository import android.os.UserHandle +import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED 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.coroutines.collectValues import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -51,6 +52,7 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { underTest = ColorCorrectionRepositoryImpl( testDispatcher, + scope.backgroundScope, settings, ) } @@ -58,83 +60,78 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) + settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - 1, + SETTING_NAME, + ENABLED, testUser1.identifier ) - - underTest = - ColorCorrectionRepositoryImpl( - testDispatcher, - settings, - ) - - underTest.isEnabled(testUser1).launchIn(backgroundScope) runCurrent() - val actualValue: Boolean = underTest.isEnabled(testUser1).first() Truth.assertThat(actualValue).isTrue() } @Test fun isEnabled_settingUpdated_valueUpdated() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1)) settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse() settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.ENABLED, + SETTING_NAME, + ENABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue() settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse() + + Truth.assertThat(flowValues.size).isEqualTo(3) + Truth.assertThat(flowValues).containsExactly(false, true, false).inOrder() } @Test fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1)) + val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2)) + settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser1.identifier ) - underTest.isEnabled(testUser2).launchIn(backgroundScope) settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser2.identifier ) - runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse() - Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse() + + Truth.assertThat(lastValueUser1).isFalse() + Truth.assertThat(lastValueUser2).isFalse() settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.ENABLED, + SETTING_NAME, + ENABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue() - Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse() + + Truth.assertThat(lastValueUser1).isTrue() + Truth.assertThat(lastValueUser2).isFalse() } @Test @@ -146,10 +143,10 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { val actualValue = settings.getIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, + SETTING_NAME, testUser1.identifier ) - Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.ENABLED) + Truth.assertThat(actualValue).isEqualTo(ENABLED) } @Test @@ -161,9 +158,15 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { val actualValue = settings.getIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, + SETTING_NAME, testUser1.identifier ) - Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.DISABLED) + Truth.assertThat(actualValue).isEqualTo(DISABLED) } + + companion object { + private const val SETTING_NAME = ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED + private const val DISABLED = 0 + private const val ENABLED = 1 + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index 423e124bbc84..4853529229fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -21,11 +21,11 @@ import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED 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.coroutines.collectValues import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -52,6 +52,7 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { underTest = ColorInversionRepositoryImpl( testDispatcher, + scope.backgroundScope, settings, ) } @@ -59,55 +60,47 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { - settings.putIntForUser(SETTING_NAME, 1, testUser1.identifier) + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) - underTest = - ColorInversionRepositoryImpl( - testDispatcher, - settings, - ) - - underTest.isEnabled(testUser1).launchIn(backgroundScope) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - val actualValue: Boolean = underTest.isEnabled(testUser1).first() assertThat(actualValue).isTrue() } @Test fun isEnabled_settingUpdated_valueUpdated() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val flowValues: List<Boolean> by + collectValues(underTest.isEnabled(testUser1)) settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isFalse() - settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isTrue() - settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isFalse() + + assertThat(flowValues.size).isEqualTo(3) + assertThat(flowValues).containsExactly(false, true, false).inOrder() } @Test fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1)) + val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2)) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) - underTest.isEnabled(testUser2).launchIn(backgroundScope) settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier) - runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isFalse() - assertThat(underTest.isEnabled(testUser2).first()).isFalse() + assertThat(lastValueUser1).isFalse() + assertThat(lastValueUser2).isFalse() settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isTrue() - assertThat(underTest.isEnabled(testUser2).first()).isFalse() + assertThat(lastValueUser1).isTrue() + assertThat(lastValueUser2).isFalse() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt index b4d4e1f51251..caf92199737c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt @@ -29,10 +29,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -59,8 +62,8 @@ class AuthenticationRepositoryTest : SysuiTestCase() { @Mock private lateinit var tableLogger: TableLogBuffer @Mock private lateinit var devicePolicyManager: DevicePolicyManager - private val testUtils = SceneTestUtils(this) - private val testScope = testUtils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val clock = FakeSystemClock() private val userRepository = FakeUserRepository() private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository @@ -82,8 +85,8 @@ class AuthenticationRepositoryTest : SysuiTestCase() { underTest = AuthenticationRepositoryImpl( applicationScope = testScope.backgroundScope, - backgroundDispatcher = testUtils.testDispatcher, - flags = testUtils.fakeSceneContainerFlags, + backgroundDispatcher = kosmos.testDispatcher, + flags = kosmos.sceneContainerFlags, clock = clock, getSecurityMode = getSecurityMode, userRepository = userRepository, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 10c16bd2f3ed..cb8cebf80767 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern @@ -29,7 +30,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,9 +47,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AuthenticationInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val underTest = utils.authenticationInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.authenticationInteractor private val onAuthenticationResult by testScope.collectLastValue(underTest.onAuthenticationResult) @@ -62,7 +64,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertThat(authMethod).isEqualTo(Pin) assertThat(underTest.getAuthenticationMethod()).isEqualTo(Pin) - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(authMethod).isEqualTo(Password) assertThat(underTest.getAuthenticationMethod()).isEqualTo(Password) @@ -74,7 +76,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val authMethod by collectLastValue(underTest.authenticationMethod) runCurrent() - utils.authenticationRepository.setAuthenticationMethod(None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(None) assertThat(authMethod).isEqualTo(None) assertThat(underTest.getAuthenticationMethod()).isEqualTo(None) @@ -83,7 +85,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPin_succeeds() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) } @@ -91,7 +93,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withIncorrectPin_fails() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertFailed(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4))) } @@ -99,7 +101,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test(expected = IllegalArgumentException::class) fun authenticate_withEmptyPin_throwsException() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) underTest.authenticate(listOf()) } @@ -107,7 +109,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun authenticate_withCorrectMaxLengthPin_succeeds() = testScope.runTest { val correctMaxLengthPin = List(16) { 9 } - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) overrideCredential(correctMaxLengthPin) } @@ -124,7 +126,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { // If the policy changes, there is work to do in SysUI. assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertFailed(underTest.authenticate(List(17) { 9 })) } @@ -132,7 +134,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPassword_succeeds() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertSucceeded(underTest.authenticate("password".toList())) } @@ -140,7 +142,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withIncorrectPassword_fails() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertFailed(underTest.authenticate("alohomora".toList())) } @@ -148,7 +150,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPattern_succeeds() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pattern) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.PATTERN)) } @@ -156,7 +158,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withIncorrectPattern_fails() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pattern) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) val wrongPattern = listOf( AuthenticationPatternCoordinate(x = 2, y = 0), @@ -172,7 +174,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNull() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -182,14 +184,14 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertSkipped(underTest.authenticate(shorterPin, tryAutoConfirm = true)) assertThat(underTest.lockoutEndTimestamp).isNull() - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(0) } @Test fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalse() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -207,7 +209,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalse() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -225,7 +227,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrue() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -241,7 +243,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) reportLockoutStarted(42) @@ -258,7 +260,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNull() = testScope.runTest { - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(false) } @@ -271,7 +273,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun tryAutoConfirm_withoutCorrectPassword_returnsNull() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertSkipped(underTest.authenticate("password".toList(), tryAutoConfirm = true)) } @@ -280,7 +282,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun isAutoConfirmEnabled_featureDisabled_returnsFalse() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(false) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(false) assertThat(isAutoConfirmEnabled).isFalse() } @@ -289,7 +291,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun isAutoConfirmEnabled_featureEnabled_returnsTrue() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(isAutoConfirmEnabled).isTrue() } @@ -298,7 +300,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun isAutoConfirmEnabled_featureEnabledButDisabledByLockout() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) // The feature is enabled. assertThat(isAutoConfirmEnabled).isTrue() @@ -308,7 +310,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertFailed(underTest.authenticate(listOf(5, 6, 7))) // Wrong PIN } assertThat(underTest.lockoutEndTimestamp).isNotNull() - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(1) // Lockout disabled auto-confirm. assertThat(isAutoConfirmEnabled).isFalse() @@ -336,7 +338,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val failedAuthenticationAttempts by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN assertSucceeded(underTest.authenticate(correctPin)) @@ -366,7 +368,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun lockoutEndTimestamp() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN underTest.authenticate(correctPin) @@ -384,7 +386,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val expectedLockoutEndTimestamp = testScope.currentTime + FakeAuthenticationRepository.LOCKOUT_DURATION_MS assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp) - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(1) // Correct PIN, but locked out, so doesn't attempt it: assertSkipped(underTest.authenticate(correctPin), assertNoResultEvents = false) @@ -409,7 +411,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun upcomingWipe() = testScope.runTest { val upcomingWipe by collectLastValue(underTest.upcomingWipe) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } @@ -418,7 +420,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { var expectedFailedAttempts = 0 var remainingFailedAttempts = - utils.authenticationRepository.getMaxFailedUnlockAttemptsForWipe() + kosmos.fakeAuthenticationRepository.getMaxFailedUnlockAttemptsForWipe() assertThat(remainingFailedAttempts) .isGreaterThan(LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) @@ -458,7 +460,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withoutAutoConfirm_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(false) } @@ -470,11 +472,13 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinTooShort_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) overrideCredential( buildList { - repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) } + repeat(kosmos.fakeAuthenticationRepository.hintedPinLength - 1) { + add(it + 1) + } } ) setAutoConfirmFeatureEnabled(true) @@ -487,28 +491,31 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinAtRightLength_isSameLength() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) overrideCredential( buildList { - repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } + repeat(kosmos.fakeAuthenticationRepository.hintedPinLength) { add(it + 1) } } ) } - assertThat(hintedPinLength).isEqualTo(utils.authenticationRepository.hintedPinLength) + assertThat(hintedPinLength) + .isEqualTo(kosmos.fakeAuthenticationRepository.hintedPinLength) } @Test fun hintedPinLength_withAutoConfirmPinTooLong_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) overrideCredential( buildList { - repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) } + repeat(kosmos.fakeAuthenticationRepository.hintedPinLength + 1) { + add(it + 1) + } } ) setAutoConfirmFeatureEnabled(true) @@ -520,10 +527,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withTooShortPassword() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) val tooShortPassword = buildList { - repeat(utils.authenticationRepository.minPasswordLength - 1) { time -> + repeat(kosmos.fakeAuthenticationRepository.minPasswordLength - 1) { time -> add("$time") } } @@ -534,7 +541,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertThat(authenticationResult).isEqualTo(AuthenticationResult.SUCCEEDED) assertThat(onAuthenticationResult).isTrue() assertThat(underTest.lockoutEndTimestamp).isNull() - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(0) assertThat(failedAuthenticationAttempts).isEqualTo(0) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 4a39799fd64f..72e884e9e5d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics +import android.graphics.Bitmap import android.hardware.biometrics.BiometricManager.Authenticators import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.PromptContentView @@ -117,6 +118,8 @@ internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): } internal fun promptInfo( + logoRes: Int = -1, + logoBitmap: Bitmap? = null, title: String = "title", subtitle: String = "sub", description: String = "desc", @@ -127,6 +130,8 @@ internal fun promptInfo( negativeButton: String = "neg", ): PromptInfo { val info = PromptInfo() + info.logoRes = logoRes + info.logoBitmap = logoBitmap info.title = title info.subtitle = subtitle info.description = description diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt index c2117ae5bda4..a67b0931f171 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt @@ -20,8 +20,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +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.test.TestScope @@ -36,8 +39,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EmergencyServicesRepositoryImplTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var underTest: EmergencyServicesRepository @@ -52,7 +55,7 @@ class EmergencyServicesRepositoryImplTest : SysuiTestCase() { EmergencyServicesRepository( resources = context.resources, applicationScope = testScope.backgroundScope, - configurationRepository = utils.configurationRepository, + configurationRepository = kosmos.configurationRepository, ) } @@ -71,7 +74,7 @@ class EmergencyServicesRepositoryImplTest : SysuiTestCase() { private fun TestScope.setEmergencyCallWhileSimLocked(isEnabled: Boolean) { overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, isEnabled) - utils.configurationRepository.onConfigurationChange() + kosmos.fakeConfigurationRepository.onConfigurationChange() runCurrent() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt index 63581b3f7070..741cde82354a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt @@ -25,11 +25,18 @@ import com.android.internal.logging.fakeMetricsLogger import com.android.internal.logging.nano.MetricsProto import com.android.internal.util.emergencyAffordanceManager import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags.REFACTOR_GETCURRENTUSER -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.whenever import com.android.telecom.telecomManager @@ -54,11 +61,11 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor @Mock private lateinit var telecomManager: TelecomManager - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val metricsLogger = utils.kosmos.fakeMetricsLogger - private val activityTaskManager = utils.kosmos.activityTaskManager - private val emergencyAffordanceManager = utils.kosmos.emergencyAffordanceManager + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val metricsLogger = kosmos.fakeMetricsLogger + private val activityTaskManager = kosmos.activityTaskManager + private val emergencyAffordanceManager = kosmos.emergencyAffordanceManager private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository @@ -68,9 +75,9 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - utils.fakeSceneContainerFlags.enabled = true + kosmos.fakeSceneContainerFlags.enabled = true - mobileConnectionsRepository = utils.mobileConnectionsRepository + mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL) overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL) @@ -83,18 +90,18 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { .thenReturn(needsEmergencyAffordance) whenever(telecomManager.isInCall).thenReturn(false) - utils.fakeFeatureFlags.set(REFACTOR_GETCURRENTUSER, true) + kosmos.fakeFeatureFlagsClassic.set(REFACTOR_GETCURRENTUSER, true) - utils.telephonyRepository.setHasTelephonyRadio(true) + kosmos.fakeTelephonyRepository.setHasTelephonyRadio(true) - utils.kosmos.telecomManager = telecomManager + kosmos.telecomManager = telecomManager } @Test fun noTelephonyRadio_noButton() = testScope.runTest { - utils.telephonyRepository.setHasTelephonyRadio(false) - val underTest = utils.bouncerActionButtonInteractor() + kosmos.fakeTelephonyRepository.setHasTelephonyRadio(false) + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) assertThat(actionButton).isNull() } @@ -102,8 +109,8 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noTelecomManager_noButton() = testScope.runTest { - utils.kosmos.telecomManager = null - val underTest = utils.bouncerActionButtonInteractor() + kosmos.telecomManager = null + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) assertThat(actionButton).isNull() } @@ -111,9 +118,9 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun duringCall_returnToCallButton() = testScope.runTest { - val underTest = utils.bouncerActionButtonInteractor() + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) - utils.telephonyRepository.setIsInCall(true) + kosmos.fakeTelephonyRepository.setIsInCall(true) assertThat(actionButton).isNotNull() assertThat(actionButton?.label).isEqualTo(MESSAGE_RETURN_TO_CALL) @@ -133,11 +140,13 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_secureAuthMethod_emergencyCallButton() = testScope.runTest { - val underTest = utils.bouncerActionButtonInteractor() + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = false - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) assertThat(actionButton).isNotNull() assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL) @@ -163,11 +172,13 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_insecureAuthMethodButSecureSim_emergencyCallButton() = testScope.runTest { - val underTest = utils.bouncerActionButtonInteractor() + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = true - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) runCurrent() assertThat(actionButton).isNotNull() @@ -179,11 +190,13 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_insecure_noButton() = testScope.runTest { - val underTest = utils.bouncerActionButtonInteractor() + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = false - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) assertThat(actionButton).isNull() } @@ -191,13 +204,15 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_simSecureButEmergencyNotSupported_noButton() = testScope.runTest { - val underTest = utils.bouncerActionButtonInteractor() + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = true overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, false) - utils.configurationRepository.onConfigurationChange() - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeConfigurationRepository.onConfigurationChange() + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) runCurrent() assertThat(actionButton).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 4b6199b55b58..707777b9f728 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -20,15 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,9 +50,9 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class BouncerInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true } - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() + private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor private lateinit var underTest: BouncerInteractor @@ -62,7 +67,7 @@ class BouncerInteractorTest : SysuiTestCase() { overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD) overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN) - underTest = utils.bouncerInteractor() + underTest = kosmos.bouncerInteractor } @Test @@ -70,7 +75,9 @@ class BouncerInteractorTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) runCurrent() underTest.clearMessage() assertThat(message).isNull() @@ -94,7 +101,9 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_sim_skipsAuthentication() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Sim + ) runCurrent() // We rely on TelephonyManager to authenticate the sim card. @@ -109,9 +118,11 @@ class BouncerInteractorTest : SysuiTestCase() { testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) runCurrent() - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(isAutoConfirmEnabled).isTrue() // Incomplete input. @@ -137,7 +148,9 @@ class BouncerInteractorTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) runCurrent() // Incomplete input. @@ -160,7 +173,7 @@ class BouncerInteractorTest : SysuiTestCase() { fun passwordAuthMethod() = testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) runCurrent() @@ -180,7 +193,8 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat( underTest.authenticate( buildList { - repeat(utils.authenticationRepository.minPasswordLength - 1) { time -> + repeat(kosmos.fakeAuthenticationRepository.minPasswordLength - 1) { time + -> add("$time") } } @@ -198,7 +212,7 @@ class BouncerInteractorTest : SysuiTestCase() { fun patternAuthMethod() = testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) runCurrent() @@ -214,7 +228,8 @@ class BouncerInteractorTest : SysuiTestCase() { AuthenticationPatternCoordinate(0, 1), ) assertThat(wrongPattern).isNotEqualTo(FakeAuthenticationRepository.PATTERN) - assertThat(wrongPattern.size).isAtLeast(utils.authenticationRepository.minPatternLength) + assertThat(wrongPattern.size) + .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength) assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED) assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) @@ -225,7 +240,7 @@ class BouncerInteractorTest : SysuiTestCase() { val tooShortPattern = FakeAuthenticationRepository.PATTERN.subList( 0, - utils.authenticationRepository.minPatternLength - 1 + kosmos.fakeAuthenticationRepository.minPatternLength - 1 ) assertThat(underTest.authenticate(tooShortPattern)) .isEqualTo(AuthenticationResult.SKIPPED) @@ -245,7 +260,9 @@ class BouncerInteractorTest : SysuiTestCase() { val lockoutStartedEvents by collectValues(underTest.onLockoutStarted) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(lockoutStartedEvents).isEmpty() // Try the wrong PIN repeatedly, until lockout is triggered: @@ -291,17 +308,17 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun intentionalUserInputEvent_registersTouchEvent() = testScope.runTest { - assertThat(utils.powerRepository.userTouchRegistered).isFalse() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() underTest.onIntentionalUserInput() - assertThat(utils.powerRepository.userTouchRegistered).isTrue() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } @Test fun intentionalUserInputEvent_notifiesFaceAuthInteractor() = testScope.runTest { val isFaceAuthRunning by - collectLastValue(utils.kosmos.fakeDeviceEntryFaceAuthRepository.isAuthRunning) - utils.kosmos.deviceEntryFaceAuthInteractor.onDeviceLifted() + collectLastValue(kosmos.fakeDeviceEntryFaceAuthRepository.isAuthRunning) + kosmos.deviceEntryFaceAuthInteractor.onDeviceLifted() runCurrent() assertThat(isFaceAuthRunning).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt index 8c53c0e3f267..09fdd11a99dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt @@ -28,8 +28,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor.Companion.INVALID_SUBSCRIPTION_ID import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -54,10 +57,10 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock lateinit var euiccManager: EuiccManager - private val utils = SceneTestUtils(this) + private val kosmos = testKosmos() private val bouncerSimRepository = FakeSimBouncerRepository() private val resources: Resources = context.resources - private val testScope = utils.testScope + private val testScope = kosmos.testScope private lateinit var underTest: SimBouncerInteractor @@ -68,13 +71,13 @@ class SimBouncerInteractorTest : SysuiTestCase() { SimBouncerInteractor( context, testScope.backgroundScope, - utils.testDispatcher, + kosmos.testDispatcher, bouncerSimRepository, telephonyManager, resources, keyguardUpdateMonitor, euiccManager, - utils.mobileConnectionsRepository, + kosmos.mobileConnectionsRepository, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index 3043a710276b..27b84b2ffabc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -20,9 +20,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest @@ -33,16 +37,16 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AuthMethodBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val bouncerInteractor = utils.bouncerInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val bouncerInteractor = kosmos.bouncerInteractor private val underTest = PinBouncerViewModel( applicationContext = context, viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, ) @@ -50,7 +54,9 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { fun animateFailure() = testScope.runTest { val animateFailure by collectLastValue(underTest.animateFailure) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(animateFailure).isFalse() // Wrong PIN: diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 4b1f9fee48bb..cfe8c5d52c18 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -20,15 +20,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlin.time.Duration.Companion.seconds @@ -50,16 +56,16 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() - private val bouncerInteractor = utils.bouncerInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor + private val bouncerInteractor = kosmos.bouncerInteractor private lateinit var underTest: BouncerViewModel @Before fun setUp() { - utils.fakeSceneContainerFlags.enabled = true - underTest = utils.bouncerViewModel() + kosmos.fakeSceneContainerFlags.enabled = true + underTest = kosmos.bouncerViewModel } @Test @@ -68,7 +74,7 @@ class BouncerViewModelTest : SysuiTestCase() { var authMethodViewModel: AuthMethodBouncerViewModel? = null authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) val job = underTest.authMethodViewModel.onEach { authMethodViewModel = it }.launchIn(this) runCurrent() @@ -98,13 +104,13 @@ class BouncerViewModelTest : SysuiTestCase() { // First pass, populate our "seen" map: authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) authMethodViewModel?.let { seen[authMethod] = it } } // Second pass, assert same instances are not reused: authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) authMethodViewModel?.let { assertThat(it.authenticationMethod).isEqualTo(authMethod) assertThat(it).isNotSameInstanceAs(seen[authMethod]) @@ -116,11 +122,11 @@ class BouncerViewModelTest : SysuiTestCase() { fun authMethodUnchanged_reusesInstances() = testScope.runTest { authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) val firstInstance: AuthMethodBouncerViewModel? = collectLastValue(underTest.authMethodViewModel).invoke() - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) val secondInstance: AuthMethodBouncerViewModel? = collectLastValue(underTest.authMethodViewModel).invoke() @@ -139,7 +145,7 @@ class BouncerViewModelTest : SysuiTestCase() { fun message() = testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(message?.isUpdateAnimated).isTrue() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { @@ -157,8 +163,8 @@ class BouncerViewModelTest : SysuiTestCase() { testScope.runTest { val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(Pin) - assertThat(utils.authenticationRepository.lockoutEndTimestamp).isNull() + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> @@ -192,7 +198,7 @@ class BouncerViewModelTest : SysuiTestCase() { authViewModel?.isInputEnabled ?: emptyFlow() } ) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isInputEnabled).isTrue() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { @@ -210,7 +216,7 @@ class BouncerViewModelTest : SysuiTestCase() { testScope.runTest { val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) val dialogViewModel by collectLastValue(underTest.dialogViewModel) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { @@ -228,17 +234,17 @@ class BouncerViewModelTest : SysuiTestCase() { fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isSideBySideSupported).isTrue() - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(isSideBySideSupported).isTrue() - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isSideBySideSupported).isTrue() - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(isSideBySideSupported).isFalse() } @@ -246,12 +252,12 @@ class BouncerViewModelTest : SysuiTestCase() { fun isFoldSplitRequired() = testScope.runTest { val isFoldSplitRequired by collectLastValue(underTest.isFoldSplitRequired) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isFoldSplitRequired).isTrue() - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(isFoldSplitRequired).isFalse() - utils.authenticationRepository.setAuthenticationMethod(Pattern) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) assertThat(isFoldSplitRequired).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index 5c5632f6aa7b..b3b6457b46e7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -19,13 +19,19 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -43,12 +49,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PasswordBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() - private val sceneInteractor = utils.sceneInteractor() - private val bouncerInteractor = utils.bouncerInteractor() - private val bouncerViewModel = utils.bouncerViewModel() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor + private val sceneInteractor = kosmos.sceneInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerViewModel = kosmos.bouncerViewModel private val isInputEnabled = MutableStateFlow(true) private val underTest = @@ -140,10 +146,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) // No input entered. @@ -309,8 +315,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.lockDeviceAndOpenPasswordBouncer() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Password + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) } @@ -320,13 +328,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { ) { if (isLockedOut) { repeat(failedAttemptCount) { - utils.authenticationRepository.reportAuthenticationAttempt(false) + kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(false) } - utils.authenticationRepository.reportLockoutStarted( + kosmos.fakeAuthenticationRepository.reportLockoutStarted( 30.seconds.inWholeMilliseconds.toInt() ) } else { - utils.authenticationRepository.reportAuthenticationAttempt(true) + kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(true) } isInputEnabled.value = !isLockedOut diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 9ee344a9527a..c2680bcc82a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -20,13 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.authenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,12 +51,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PatternBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() - private val sceneInteractor = utils.sceneInteractor() - private val bouncerInteractor = utils.bouncerInteractor() - private val bouncerViewModel = utils.bouncerViewModel() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor + private val sceneInteractor = kosmos.sceneInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerViewModel = kosmos.bouncerViewModel private val underTest = PatternBouncerViewModel( applicationContext = context, @@ -305,7 +312,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragStart() CORRECT_PATTERN.subList( 0, - utils.authenticationRepository.minPatternLength - 1, + kosmos.authenticationRepository.minPatternLength - 1, ) .forEach { coordinate -> underTest.onDrag( @@ -374,8 +381,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.lockDeviceAndOpenPatternBouncer() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pattern + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 75e372f29590..1d660d63710d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -20,12 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -43,19 +51,19 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PinBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() - private val authenticationInteractor = utils.authenticationInteractor() - private val bouncerInteractor = utils.bouncerInteractor() - private val bouncerViewModel = utils.bouncerViewModel() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val authenticationInteractor = kosmos.authenticationInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerViewModel = kosmos.bouncerViewModel private val underTest = PinBouncerViewModel( applicationContext = context, viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, ) @@ -86,7 +94,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, ) @@ -97,7 +105,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onErrorDialogDismissed_clearsDialogMessage() = testScope.runTest { val dialogMessage by collectLastValue(underTest.errorDialogMessage) - utils.simBouncerRepository.setSimVerificationErrorMessage("abc") + kosmos.fakeSimBouncerRepository.setSimVerificationErrorMessage("abc") assertThat(dialogMessage).isEqualTo("abc") underTest.onErrorDialogDismissed() @@ -114,10 +122,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, ) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) assertThat(hintedPinLength).isNull() @@ -254,7 +262,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenCorrect() = testScope.runTest { - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) lockDeviceAndOpenPinBouncer() @@ -269,7 +277,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.desiredScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) lockDeviceAndOpenPinBouncer() FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit -> @@ -309,7 +317,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Shown) } @@ -318,8 +328,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun backspaceButtonAppearance_withAutoConfirmButNoInput_isHidden() = testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } @@ -328,8 +340,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun backspaceButtonAppearance_withAutoConfirmAndInput_isShownQuiet() = testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) underTest.onPinButtonClicked(1) @@ -341,7 +355,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Shown) } @@ -350,8 +366,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun confirmButtonAppearance_withAutoConfirm_isHidden() = testScope.runTest { val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } @@ -361,10 +379,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val isAnimationEnabled by collectLastValue(underTest.isDigitButtonAnimationEnabled) - utils.authenticationRepository.setPinEnhancedPrivacyEnabled(true) + kosmos.fakeAuthenticationRepository.setPinEnhancedPrivacyEnabled(true) assertThat(isAnimationEnabled).isFalse() - utils.authenticationRepository.setPinEnhancedPrivacyEnabled(false) + kosmos.fakeAuthenticationRepository.setPinEnhancedPrivacyEnabled(false) assertThat(isAnimationEnabled).isTrue() } @@ -382,8 +400,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.lockDeviceAndOpenPinBouncer() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) } 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 new file mode 100644 index 000000000000..820bfbfdf0a2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.content.SharedPreferences +import android.content.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryImpl.Companion.FILE_NAME +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.UserFileManager +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.FakeSharedPreferences +import com.google.common.truth.Truth.assertThat +import java.io.File +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalPrefsRepositoryImplTest : SysuiTestCase() { + private lateinit var underTest: CommunalPrefsRepositoryImpl + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var userRepository: FakeUserRepository + private lateinit var userFileManager: UserFileManager + + @Before + fun setUp() { + userRepository = kosmos.fakeUserRepository + userRepository.setUserInfos(USER_INFOS) + + userFileManager = + FakeUserFileManager( + mapOf( + USER_INFOS[0].id to FakeSharedPreferences(), + USER_INFOS[1].id to FakeSharedPreferences() + ) + ) + underTest = + CommunalPrefsRepositoryImpl( + testScope.backgroundScope, + kosmos.testDispatcher, + userRepository, + userFileManager, + ) + } + + @Test + fun isCtaDismissedValue_byDefault_isFalse() = + testScope.runTest { + val isCtaDismissed by collectLastValue(underTest.isCtaDismissed) + assertThat(isCtaDismissed).isFalse() + } + + @Test + fun isCtaDismissedValue_onSet_isTrue() = + testScope.runTest { + val isCtaDismissed by collectLastValue(underTest.isCtaDismissed) + + underTest.setCtaDismissedForCurrentUser() + assertThat(isCtaDismissed).isTrue() + } + + @Test + fun isCtaDismissedValue_whenSwitchUser() = + testScope.runTest { + val isCtaDismissed by collectLastValue(underTest.isCtaDismissed) + underTest.setCtaDismissedForCurrentUser() + + // dismissed true for primary user + assertThat(isCtaDismissed).isTrue() + + // switch to secondary user + userRepository.setSelectedUserInfo(USER_INFOS[1]) + + // dismissed is false for secondary user + assertThat(isCtaDismissed).isFalse() + + // switch back to primary user + userRepository.setSelectedUserInfo(USER_INFOS[0]) + + // dismissed is true for primary user + assertThat(isCtaDismissed).isTrue() + } + + private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) : + UserFileManager { + override fun getFile(fileName: String, userId: Int): File { + throw UnsupportedOperationException() + } + + override fun getSharedPreferences( + fileName: String, + mode: Int, + userId: Int + ): SharedPreferences { + if (fileName != FILE_NAME) { + throw IllegalArgumentException("Preference files must be $FILE_NAME") + } + return sharedPrefs.getValue(userId) + } + } + + companion object { + val USER_INFOS = + listOf( + UserInfo(/* id= */ 0, "zero", /* flags= */ 0), + UserInfo(/* id= */ 1, "secondary", /* flags= */ 0), + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt index 65176e1c5c0d..81d5344ed264 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt @@ -24,11 +24,12 @@ import com.android.systemui.communal.shared.model.ObservableCommunalTransitionSt import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.data.repository.SceneContainerRepository +import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher @@ -51,8 +52,8 @@ class CommunalRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { - val sceneTestUtils = SceneTestUtils(this) - sceneContainerRepository = sceneTestUtils.fakeSceneContainerRepository() + val kosmos = testKosmos() + sceneContainerRepository = kosmos.sceneContainerRepository featureFlagsClassic = FakeFeatureFlagsClassic() featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt new file mode 100644 index 000000000000..c4a8582d51b5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt @@ -0,0 +1,145 @@ +/* + * 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.communal.data.repository + +import android.content.pm.UserInfo +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.communal.data.repository.CommunalTutorialRepositoryImpl.Companion.CURRENT_TUTORIAL_VERSION +import com.android.systemui.coroutines.collectLastValue +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.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalTutorialRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var secureSettings: FakeSettings + private lateinit var userRepository: FakeUserRepository + + private lateinit var underTest: CommunalTutorialRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + secureSettings = FakeSettings() + userRepository = FakeUserRepository() + val listOfUserInfo = listOf(MAIN_USER_INFO) + userRepository.setUserInfos(listOfUserInfo) + + underTest = + CommunalTutorialRepositoryImpl( + kosmos.applicationCoroutineScope, + kosmos.testDispatcher, + userRepository, + secureSettings, + logcatLogBuffer("CommunalTutorialRepositoryImplTest"), + ) + } + + @Test + fun tutorialSettingState_defaultToNotStarted() = + testScope.runTest { + val tutorialSettingState by collectLastValue(underTest.tutorialSettingState) + assertThat(tutorialSettingState) + .isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED) + } + + @Test + fun tutorialSettingState_whenTutorialSettingsUpdatedToStarted() = + testScope.runTest { + underTest.setTutorialState(Settings.Secure.HUB_MODE_TUTORIAL_STARTED) + val tutorialSettingState by collectLastValue(underTest.tutorialSettingState) + assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_STARTED) + } + + @Test + fun tutorialSettingState_whenTutorialSettingsUpdatedToCompleted() = + testScope.runTest { + underTest.setTutorialState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + val tutorialSettingState by collectLastValue(underTest.tutorialSettingState) + assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + } + + @Test + fun tutorialVersion_userCompletedCurrentVersion_stateCompleted() = + testScope.runTest { + // User completed the current version. + setTutorialStateSetting(CURRENT_TUTORIAL_VERSION) + + // Verify tutorial state is completed. + val tutorialSettingState by collectLastValue(underTest.tutorialSettingState) + assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + } + + @Test + fun tutorialVersion_userCompletedPreviousVersion_stateNotStarted() = + testScope.runTest { + // User completed the previous version. + setTutorialStateSetting(CURRENT_TUTORIAL_VERSION - 1) + + // Verify tutorial state is not started. + val tutorialSettingState by collectLastValue(underTest.tutorialSettingState) + assertThat(tutorialSettingState) + .isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED) + } + + @Test + fun tutorialVersion_uponTutorialCompletion_writeCurrentVersion() = + testScope.runTest { + // Tutorial not started. + setTutorialStateSetting(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED) + + // Tutorial completed. + underTest.setTutorialState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + + // Verify tutorial setting state is updated to current version. + val settingState = getTutorialStateSetting() + assertThat(settingState).isEqualTo(CURRENT_TUTORIAL_VERSION) + } + + private fun setTutorialStateSetting( + @Settings.Secure.HubModeTutorialState state: Int, + user: UserInfo = MAIN_USER_INFO + ) { + secureSettings.putIntForUser(Settings.Secure.HUB_MODE_TUTORIAL_STATE, state, user.id) + } + + private fun getTutorialStateSetting(user: UserInfo = MAIN_USER_INFO): Int { + return secureSettings.getIntForUser(Settings.Secure.HUB_MODE_TUTORIAL_STATE, user.id) + } + + companion object { + private val MAIN_USER_INFO = + UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index 4079f1241f31..fdb17c298e17 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -16,14 +16,9 @@ package com.android.systemui.communal.data.repository -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName -import android.content.Intent -import android.content.Intent.ACTION_USER_UNLOCKED -import android.os.UserHandle -import android.os.UserManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -32,11 +27,14 @@ import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.log.LogBuffer -import com.android.systemui.log.core.FakeLogBuffer +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R -import com.android.systemui.settings.UserTracker +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 @@ -44,8 +42,6 @@ import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -65,13 +61,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var appWidgetManager: AppWidgetManager - @Mock private lateinit var appWidgetHost: AppWidgetHost - - @Mock private lateinit var userManager: UserManager - - @Mock private lateinit var userHandle: UserHandle - - @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo @@ -81,13 +71,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var communalWidgetDao: CommunalWidgetDao - private lateinit var communalRepository: FakeCommunalRepository + private val kosmos = testKosmos() - private lateinit var logBuffer: LogBuffer + private val testScope = kosmos.testScope - private val testDispatcher = StandardTestDispatcher() - - private val testScope = TestScope(testDispatcher) + private lateinit var logBuffer: LogBuffer private val fakeAllowlist = listOf( @@ -96,68 +84,60 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { "com.android.fake/WidgetProviderC", ) + private lateinit var underTest: CommunalWidgetRepositoryImpl + @Before fun setUp() { MockitoAnnotations.initMocks(this) - logBuffer = FakeLogBuffer.Factory.create() - communalRepository = FakeCommunalRepository() + logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") - communalEnabled(true) setAppWidgetIds(emptyList()) overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") - whenever(userTracker.userHandle).thenReturn(userHandle) whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap())) whenever(appWidgetManagerOptional.isPresent).thenReturn(true) whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager) + + underTest = + CommunalWidgetRepositoryImpl( + appWidgetManagerOptional, + appWidgetHost, + testScope.backgroundScope, + kosmos.testDispatcher, + communalWidgetHost, + communalWidgetDao, + logBuffer, + ) } @Test - fun neverQueryDbForWidgets_whenFeatureIsDisabled() = + fun neverQueryDbForWidgets_whenHostIsInactive() = testScope.runTest { - communalEnabled(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) + underTest.updateAppWidgetHostActive(false) + underTest.communalWidgets.launchIn(testScope.backgroundScope) runCurrent() verify(communalWidgetDao, never()).getWidgets() } @Test - fun neverQueryDbForWidgets_whenFeatureEnabled_andUserLocked() = + fun communalWidgets_whenHostIsActive_queryWidgetsFromDb() = testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - - verify(communalWidgetDao, never()).getWidgets() - } + underTest.updateAppWidgetHostActive(true) - @Test - fun communalWidgets_whenUserUnlocked_queryWidgetsFromDb() = - testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - val communalWidgets by collectLastValue(repository.communalWidgets) - runCurrent() val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) whenever(communalWidgetDao.getWidgets()) .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry))) whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) - userUnlocked(true) installedProviders(listOf(stopwatchProviderInfo)) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + val communalWidgets by collectLastValue(underTest.communalWidgets) + runCurrent() verify(communalWidgetDao).getWidgets() assertThat(communalWidgets) .containsExactly( @@ -172,9 +152,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_allocateId_bindWidget_andAddToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 @@ -182,7 +160,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) - repository.addWidget(provider, priority) { true } + underTest.addWidget(provider, priority) { true } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -192,16 +170,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - repository.addWidget(provider, priority) { false } + underTest.addWidget(provider, priority) { false } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -212,16 +188,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - repository.addWidget(provider, priority) { throw IllegalStateException("some error") } + underTest.addWidget(provider, priority) { throw IllegalStateException("some error") } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -232,9 +206,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val provider = ComponentName("pkg_name", "cls_name") val id = 1 @@ -243,7 +215,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) var configured = false - repository.addWidget(provider, priority) { + underTest.addWidget(provider, priority) { configured = true true } @@ -257,12 +229,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val id = 1 - repository.deleteWidget(id) + underTest.deleteWidget(id) runCurrent() verify(communalWidgetDao).deleteWidgetById(id) @@ -272,108 +242,37 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun reorderWidgets_queryDb() = testScope.runTest { - userUnlocked(true) - val repository = initCommunalWidgetRepository() - runCurrent() + underTest.updateAppWidgetHostActive(true) val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) - repository.updateWidgetOrder(widgetIdToPriorityMap) + underTest.updateWidgetOrder(widgetIdToPriorityMap) runCurrent() verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } @Test - fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = + fun appWidgetHost_startListening() = testScope.runTest { - communalEnabled(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0) - } - - @Test - fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() = - testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1) - } - - @Test - fun appWidgetHost_userUnlocked_startListening() = - testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() verify(appWidgetHost, never()).startListening() - userUnlocked(true) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + underTest.updateAppWidgetHostActive(true) verify(appWidgetHost).startListening() } @Test - fun appWidgetHost_userLockedAgain_stopListening() = + fun appWidgetHost_stopListening() = testScope.runTest { - userUnlocked(false) - val repository = initCommunalWidgetRepository() - repository.communalWidgets.launchIn(backgroundScope) - runCurrent() - - userUnlocked(true) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + underTest.updateAppWidgetHostActive(true) verify(appWidgetHost).startListening() - verify(appWidgetHost, never()).stopListening() - userUnlocked(false) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(ACTION_USER_UNLOCKED) - ) - runCurrent() + underTest.updateAppWidgetHostActive(false) verify(appWidgetHost).stopListening() } - private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl { - return CommunalWidgetRepositoryImpl( - appWidgetManagerOptional, - appWidgetHost, - testScope.backgroundScope, - testDispatcher, - fakeBroadcastDispatcher, - communalRepository, - communalWidgetHost, - communalWidgetDao, - userManager, - userTracker, - logBuffer, - ) - } - - private fun communalEnabled(enabled: Boolean) { - communalRepository.setIsCommunalEnabled(enabled) - } - - private fun userUnlocked(userUnlocked: Boolean) { - whenever(userManager.isUserUnlockingOrUnlocked(userHandle)).thenReturn(userUnlocked) - } - private fun installedProviders(providers: List<AppWidgetProviderInfo>) { whenever(appWidgetManager.installedProviders).thenReturn(providers) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt new file mode 100644 index 000000000000..ed29aa4ac202 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -0,0 +1,95 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.communalInteractor +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.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This class is a variation of the [CommunalInteractorTest] for cases where communal is disabled. + */ +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var communalRepository: FakeCommunalRepository + private lateinit var widgetRepository: FakeCommunalWidgetRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + + private lateinit var underTest: CommunalInteractor + + @Before + fun setUp() { + communalRepository = kosmos.fakeCommunalRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + keyguardRepository = kosmos.fakeKeyguardRepository + + communalRepository.setIsCommunalEnabled(false) + + underTest = kosmos.communalInteractor + } + + @Test + fun isCommunalEnabled_false() = + testScope.runTest { assertThat(underTest.isCommunalEnabled).isFalse() } + + @Test + fun isCommunalAvailable_whenStorageUnlock_false() = + testScope.runTest { + val isCommunalAvailable by collectLastValue(underTest.isCommunalAvailable) + + assertThat(isCommunalAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(isCommunalAvailable).isFalse() + } + + @Test + fun updateAppWidgetHostActive_whenStorageUnlock_false() = + testScope.runTest { + assertThat(widgetRepository.isHostActive()).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(widgetRepository.isHostActive()).isFalse() + } +} 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 cd83c07a3b38..7769223c8af2 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 @@ -24,9 +24,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalSceneKey @@ -35,15 +41,19 @@ import com.android.systemui.communal.shared.model.ObservableCommunalTransitionSt import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.communalInteractor +import com.android.systemui.keyguard.domain.interactor.editWidgetsActivityStarter +import com.android.systemui.kosmos.testScope import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever 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.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -51,13 +61,17 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations +/** + * This class of test cases assume that communal is enabled. For disabled cases, see + * [CommunalInteractorCommunalDisabledTest]. + */ @SmallTest @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class CommunalInteractorTest : SysuiTestCase() { - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var communalRepository: FakeCommunalRepository @@ -65,41 +79,63 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter private lateinit var underTest: CommunalInteractor @Before fun setUp() { - MockitoAnnotations.initMocks(this) + tutorialRepository = kosmos.fakeCommunalTutorialRepository + communalRepository = kosmos.fakeCommunalRepository + mediaRepository = kosmos.fakeCommunalMediaRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + smartspaceRepository = kosmos.fakeSmartspaceRepository + keyguardRepository = kosmos.fakeKeyguardRepository + editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter + communalPrefsRepository = kosmos.fakeCommunalPrefsRepository + + underTest = kosmos.communalInteractor + } - testScope = TestScope(StandardTestDispatcher()) + @Test + fun communalEnabled_true() = + testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() } - val withDeps = CommunalInteractorFactory.create(testScope) + @Test + fun isCommunalAvailable_trueWhenStorageUnlock() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() - tutorialRepository = withDeps.tutorialRepository - communalRepository = withDeps.communalRepository - mediaRepository = withDeps.mediaRepository - widgetRepository = withDeps.widgetRepository - smartspaceRepository = withDeps.smartspaceRepository - keyguardRepository = withDeps.keyguardRepository - editWidgetsActivityStarter = withDeps.editWidgetsActivityStarter + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() - underTest = withDeps.communalInteractor - } + assertThat(isAvailable).isTrue() + } @Test - fun communalEnabled() = + fun isCommunalAvailable_whenStorageUnlock_true() = testScope.runTest { - communalRepository.setIsCommunalEnabled(true) - assertThat(underTest.isCommunalEnabled).isTrue() + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(isAvailable).isTrue() } @Test - fun communalDisabled() = + fun updateAppWidgetHostActive_uponStorageUnlock_true() = testScope.runTest { - communalRepository.setIsCommunalEnabled(false) - assertThat(underTest.isCommunalEnabled).isFalse() + collectLastValue(underTest.isCommunalAvailable) + assertThat(widgetRepository.isHostActive()).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + runCurrent() + + assertThat(widgetRepository.isHostActive()).isTrue() } @Test @@ -331,10 +367,9 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun cta_visibilityTrue_shows() = + fun ctaTile_showsByDefault() = testScope.runTest { tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(true) val ctaTileContent by collectLastValue(underTest.ctaTileContent) @@ -346,10 +381,10 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun ctaTile_visibilityFalse_doesNotShow() = + fun ctaTile_afterDismiss_doesNotShow() = testScope.runTest { tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(false) + communalPrefsRepository.setCtaDismissedForCurrentUser() val ctaTileContent by collectLastValue(underTest.ctaTileContent) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt new file mode 100644 index 000000000000..e904236f8c3e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt @@ -0,0 +1,59 @@ +/* + * 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.communal.ui.widgets + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.CommunalAppWidgetHostView +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +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 CommunalAppWidgetHostTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: CommunalAppWidgetHost + + @Before + fun setUp() { + underTest = CommunalAppWidgetHost(context = context, hostId = 116) + } + + @Test + fun createViewForCommunal_returnCommunalAppWidgetView() = + testScope.runTest { + val appWidgetId = 789 + val view = + underTest.createViewForCommunal( + context = context, + appWidgetId = appWidgetId, + appWidget = null + ) + assertThat(view).isInstanceOf(CommunalAppWidgetHostView::class.java) + assertThat(view).isNotNull() + assertThat(view.appWidgetId).isEqualTo(appWidgetId) + } +} 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 54510a82201a..09243e5282da 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 @@ -19,7 +19,6 @@ package com.android.systemui.communal.view.viewmodel import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.app.smartspace.SmartspaceTarget -import android.appwidget.AppWidgetHost import android.content.ComponentName import android.provider.Settings import android.widget.RemoteViews @@ -36,6 +35,7 @@ import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.testScope @@ -61,7 +61,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost - @Mock private lateinit var appWidgetHost: AppWidgetHost + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var uiEventLogger: UiEventLogger private val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 804c0528c46c..f9cfc3732a01 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository @@ -66,6 +67,7 @@ class CommunalViewModelTest : SysuiTestCase() { private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository + private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var underTest: CommunalViewModel @@ -82,6 +84,7 @@ class CommunalViewModelTest : SysuiTestCase() { widgetRepository = withDeps.widgetRepository smartspaceRepository = withDeps.smartspaceRepository mediaRepository = withDeps.mediaRepository + communalPrefsRepository = withDeps.communalPrefsRepository underTest = CommunalViewModel( @@ -149,9 +152,6 @@ class CommunalViewModelTest : SysuiTestCase() { // Media playing. mediaRepository.mediaActive() - // CTA Tile not dismissed. - communalRepository.setCtaTileInViewModeVisibility(true) - val communalContent by collectLastValue(underTest.communalContent) // Order is smart space, then UMO, widget content and cta tile. @@ -171,7 +171,6 @@ class CommunalViewModelTest : SysuiTestCase() { fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(true) val communalContent by collectLastValue(underTest.communalContent) val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) @@ -195,7 +194,6 @@ class CommunalViewModelTest : SysuiTestCase() { fun popup_onDismiss_hidesImmediately() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(true) val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt index 565049baf6f4..b54c5bdae004 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt @@ -7,9 +7,11 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever @@ -36,8 +38,8 @@ class DeviceEntryRepositoryTest : SysuiTestCase() { @Mock private lateinit var keyguardBypassController: KeyguardBypassController @Mock private lateinit var keyguardStateController: KeyguardStateController - private val testUtils = SceneTestUtils(this) - private val testScope = testUtils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val userRepository = FakeUserRepository() private val keyguardRepository = FakeKeyguardRepository() @@ -52,7 +54,7 @@ class DeviceEntryRepositoryTest : SysuiTestCase() { underTest = DeviceEntryRepositoryImpl( applicationScope = testScope.backgroundScope, - backgroundDispatcher = testUtils.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, userRepository = userRepository, lockPatternUtils = lockPatternUtils, keyguardBypassController = keyguardBypassController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 929e879a5406..62d23152b77a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -20,14 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeTrustRepository -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -41,18 +47,18 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DeviceEntryInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val faceAuthRepository = utils.kosmos.fakeDeviceEntryFaceAuthRepository - private val trustRepository = utils.kosmos.fakeTrustRepository - private val sceneInteractor = utils.sceneInteractor() - private val authenticationInteractor = utils.authenticationInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + private val trustRepository = kosmos.fakeTrustRepository + private val sceneInteractor = kosmos.sceneInteractor + private val authenticationInteractor = kosmos.authenticationInteractor private lateinit var underTest: DeviceEntryInteractor @Before fun setUp() { - utils.fakeSceneContainerFlags.enabled = true - underTest = utils.deviceEntryInteractor() + kosmos.fakeSceneContainerFlags.enabled = true + underTest = kosmos.deviceEntryInteractor } @Test @@ -65,8 +71,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isUnlocked_whenAuthMethodIsNoneAndLockscreenDisabled_isTrue() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.deviceEntryRepository.apply { + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeDeviceEntryRepository.apply { setLockscreenEnabled(false) // Toggle isUnlocked, twice. @@ -99,8 +107,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isUnlocked_whenAuthMethodIsSimAndUnlocked_isFalse() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Sim + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) val isUnlocked by collectLastValue(underTest.isUnlocked) assertThat(isUnlocked).isFalse() @@ -157,10 +167,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isDeviceEntered_onBouncer_isFalse() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) switchToScene(SceneKey.Lockscreen) runCurrent() switchToScene(SceneKey.Bouncer) @@ -182,8 +192,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun canSwipeToEnter_onLockscreenWithPin_isFalse() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) switchToScene(SceneKey.Lockscreen) val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter) @@ -203,15 +215,15 @@ class DeviceEntryInteractorTest : SysuiTestCase() { } private fun setupSwipeDeviceEntryMethod() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) } @Test fun canSwipeToEnter_whenTrustedByTrustManager_isTrue() = testScope.runTest { val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) switchToScene(SceneKey.Lockscreen) @@ -228,7 +240,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { fun canSwipeToEnter_whenAuthenticatedByFace_isTrue() = testScope.runTest { val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) switchToScene(SceneKey.Lockscreen) @@ -244,9 +256,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_lockedAndSecured_true() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) @@ -256,9 +268,11 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_lockedAndNotSecured_false() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) assertThat(underTest.isAuthenticationRequired()).isFalse() } @@ -266,9 +280,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndSecured_false() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) @@ -278,9 +292,11 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndNotSecured_false() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) assertThat(underTest.isAuthenticationRequired()).isFalse() } @@ -288,7 +304,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isBypassEnabled_enabledInRepository_true() = testScope.runTest { - utils.deviceEntryRepository.setBypassEnabled(true) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(true) assertThat(underTest.isBypassEnabled.value).isTrue() } @@ -299,8 +315,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { switchToScene(SceneKey.Lockscreen) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() underTest.attemptDeviceEntry() @@ -315,7 +333,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { switchToScene(SceneKey.Lockscreen) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) underTest.attemptDeviceEntry() @@ -329,8 +349,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { switchToScene(SceneKey.Lockscreen) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) underTest.attemptDeviceEntry() @@ -340,7 +362,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isBypassEnabled_disabledInRepository_false() = testScope.runTest { - utils.deviceEntryRepository.setBypassEnabled(false) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(false) assertThat(underTest.isBypassEnabled.value).isFalse() } @@ -348,8 +370,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { fun successfulAuthenticationChallengeAttempt_updatesIsUnlockedState() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) assertThat(isUnlocked).isFalse() authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index c4ebbdcc2f58..6f62afc560fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -52,6 +53,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify @@ -68,6 +71,8 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController + @Mock private lateinit var userTracker: UserTracker + @Captor private lateinit var updateCallbackCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback> private val mainDispatcher = StandardTestDispatcher() private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -93,6 +98,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { testScope.backgroundScope, systemClock, facePropertyRepository, + userTracker, ) } @@ -553,4 +559,25 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { job.cancel() } + + @Test + fun isEncryptedOrLockdown() = + testScope.runTest { + whenever(userTracker.userId).thenReturn(0) + whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(0)).thenReturn(true) + + // Default value for isEncryptedOrLockdown is true + val isEncryptedOrLockdown by collectLastValue(underTest.isEncryptedOrLockdown) + assertThat(isEncryptedOrLockdown).isTrue() + + verify(keyguardUpdateMonitor).registerCallback(updateCallbackCaptor.capture()) + val updateCallback = updateCallbackCaptor.value + + // Strong auth state updated + whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(0)).thenReturn(false) + updateCallback.onStrongAuthStateChanged(0) + + // Verify no longer encrypted or lockdown + assertThat(isEncryptedOrLockdown).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index 8933d2c93f5a..2c3afb1b40a9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -26,12 +26,16 @@ import com.android.systemui.common.ui.data.repository.FakeConfigurationRepositor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -48,10 +52,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class KeyguardInteractorTest : SysuiTestCase() { - private val testUtils = SceneTestUtils(this) - private val testScope = testUtils.testScope - private val repository = testUtils.keyguardRepository - private val sceneInteractor = testUtils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val repository = kosmos.fakeKeyguardRepository + private val sceneInteractor = kosmos.sceneInteractor private val commandQueue = FakeCommandQueue() private val bouncerRepository = FakeKeyguardBouncerRepository() private val shadeRepository = FakeShadeRepository() @@ -63,7 +67,7 @@ class KeyguardInteractorTest : SysuiTestCase() { repository = repository, commandQueue = commandQueue, powerInteractor = PowerInteractorFactory.create().powerInteractor, - sceneContainerFlags = testUtils.fakeSceneContainerFlags, + sceneContainerFlags = kosmos.fakeSceneContainerFlags, bouncerRepository = bouncerRepository, configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()), shadeRepository = shadeRepository, @@ -183,7 +187,7 @@ class KeyguardInteractorTest : SysuiTestCase() { @Test fun animationDozingTransitions() = testScope.runTest { - testUtils.fakeSceneContainerFlags.enabled = true + kosmos.fakeSceneContainerFlags.enabled = true val isAnimate by collectLastValue(underTest.animateDozingTransitions) underTest.setAnimateDozingTransitions(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 4f7d9444020c..6828041eff5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -21,23 +21,24 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER 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 junit.framework.Assert.assertEquals -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -46,18 +47,11 @@ import org.junit.runner.RunWith @kotlinx.coroutines.ExperimentalCoroutinesApi class KeyguardTransitionInteractorTest : SysuiTestCase() { - private lateinit var underTest: KeyguardTransitionInteractor - private lateinit var repository: FakeKeyguardTransitionRepository - private val testScope = TestScope() - - @Before - fun setUp() { - repository = FakeKeyguardTransitionRepository() - underTest = KeyguardTransitionInteractorFactory.create( - scope = testScope.backgroundScope, - repository = repository, - ).keyguardTransitionInteractor - } + val kosmos = testKosmos() + + val underTest = kosmos.keyguardTransitionInteractor + val repository = kosmos.fakeKeyguardTransitionRepository + val testScope = kosmos.testScope @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = runTest { @@ -114,49 +108,51 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun finishedKeyguardStateTests() = testScope.runTest { - val finishedSteps by collectValues(underTest.finishedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) + fun finishedKeyguardStateTests() = + testScope.runTest { + val finishedSteps by collectValues(underTest.finishedKeyguardState) runCurrent() + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) } - assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) - } - @Test - fun startedKeyguardStateTests() = testScope.runTest { - val startedStates by collectValues(underTest.startedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) + fun startedKeyguardStateTests() = + testScope.runTest { + val startedStates by collectValues(underTest.startedKeyguardState) runCurrent() + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) } - assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) - } - @Test fun finishedKeyguardTransitionStepTests() = runTest { val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep) @@ -178,7 +174,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { // Ignore the default state. assertThat(finishedSteps.subList(1, finishedSteps.size)) - .isEqualTo(listOf(steps[2], steps[5])) + .isEqualTo(listOf(steps[2], steps[5])) } @Test @@ -233,500 +229,1067 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun isInTransitionToState() = testScope.runTest { - val results by collectValues(underTest.isInTransitionToState(GONE)) + fun isInTransitionToAnyState() = + testScope.runTest { + val inTransition by collectValues(underTest.isInTransitionToAnyState) + + assertEquals( + listOf( + true, // The repo is seeded with a transition from OFF to LOCKSCREEN. + false, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + ) + + assertEquals( + listOf( + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + ), + inTransition + ) + } + + @Test + fun isInTransitionToAnyState_finishedStateIsStartedStateAfterCancels() = + testScope.runTest { + val inTransition by collectValues(underTest.isInTransitionToAnyState) + + assertEquals( + listOf( + true, + false, + ), + inTransition + ) + + // Start FINISHED in GONE. + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + ), + inTransition + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0.5f, RUNNING), + TransitionStep(GONE, DOZING, 0.6f, CANCELED), + TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED), + TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING), + TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED), + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + // We should have been in transition throughout the entire transition, including + // both cancellations, and we should still be in transition despite now + // transitioning to GONE, the state we're also FINISHED in. + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + true, + false, + ), + inTransition + ) + } + + @Test + fun isInTransitionToState() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionToState(GONE)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionFromState() = testScope.runTest { - val results by collectValues(underTest.isInTransitionFromState(DOZING)) + fun isInTransitionFromState() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionFromState(DOZING)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionFromStateWhere() = testScope.runTest { - val results by collectValues(underTest.isInTransitionFromStateWhere { - it == DOZING - }) + fun isInTransitionFromStateWhere() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionFromStateWhere { it == DOZING }) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionWhere() = testScope.runTest { - val results by collectValues(underTest.isInTransitionWhere( - fromStatePredicate = { it == DOZING }, - toStatePredicate = { it == GONE }, - )) + fun isInTransitionWhere() = + testScope.runTest { + val results by + collectValues( + underTest.isInTransitionWhere( + fromStatePredicate = { it == DOZING }, + toStatePredicate = { it == GONE }, + ) + ) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isFinishedInStateWhere() = testScope.runTest { - val results by collectValues(underTest.isFinishedInStateWhere { it == GONE } ) + fun isInTransitionWhere_withCanceledStep() = + testScope.runTest { + val results by + collectValues( + underTest.isInTransitionWhere( + fromStatePredicate = { it == DOZING }, + toStatePredicate = { it == GONE }, + ) + ) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, // Finished in DOZING, not GONE. - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, STARTED), + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, RUNNING), + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, CANCELED), + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), - ) + TransitionStep(GONE, DOZING, 1f, FINISHED), + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps( + TransitionStep(DOZING, GONE, 0f, STARTED), + TransitionStep(DOZING, GONE, 0f, RUNNING), + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } + + @Test + fun isFinishedInStateWhere() = + testScope.runTest { + val results by collectValues(underTest.isFinishedInStateWhere { it == GONE }) + + sendSteps( + TransitionStep(AOD, DOZING, 0f, STARTED), + TransitionStep(AOD, DOZING, 0.5f, RUNNING), + TransitionStep(AOD, DOZING, 1f, FINISHED), + ) + + assertThat(results) + .isEqualTo( + listOf( + false, // Finished in DOZING, not GONE. + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + TransitionStep(GONE, DOZING, 0f, RUNNING), + ) - sendSteps( + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) + + sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) - - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isFinishedInState() = testScope.runTest { - val results by collectValues(underTest.isFinishedInState(GONE)) + fun isFinishedInState() = + testScope.runTest { + val results by collectValues(underTest.isFinishedInState(GONE)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, // Finished in DOZING, not GONE. - )) + assertThat(results) + .isEqualTo( + listOf( + false, // Finished in DOZING, not GONE. + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) - - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = testScope.runTest { - val finishedStates by collectValues(underTest.finishedKeyguardState) + fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = + testScope.runTest { + val finishedStates by collectValues(underTest.finishedKeyguardState) - // We default FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN - ), finishedStates) + // We default FINISHED in LOCKSCREEN. + assertEquals(listOf(LOCKSCREEN), finishedStates) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED), - ) + ) - // We're FINISHED in AOD. - assertEquals(listOf( - LOCKSCREEN, - AOD, - ), finishedStates) + // We're FINISHED in AOD. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + ), + finishedStates + ) - // Transition back to LOCKSCREEN. - sendSteps( + // Transition back to LOCKSCREEN. + sendSteps( TransitionStep(AOD, LOCKSCREEN, 0f, STARTED), TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING), TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED), - ) + ) - // We're FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We're FINISHED in LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), - ) + ) - // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in - // LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in + // LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED), - ) + ) - // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED), TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING), TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED), - ) - - // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to - // LOCKSCREEN after the cancellation. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - LOCKSCREEN, - ), finishedStates) - } + ) + + // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to + // LOCKSCREEN after the cancellation. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + LOCKSCREEN, + ), + finishedStates + ) + } + + @Test + fun testCurrentState() = + testScope.runTest { + val currentStates by collectValues(underTest.currentKeyguardState) + + // We init the repo with a transition from OFF -> LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), + ) + + // The current state should continue to be LOCKSCREEN as we transition to AOD. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), + ) + + // The current state should continue to be LOCKSCREEN as we transition to AOD. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED), + ) + + // Once CANCELED, we're still currently in LOCKSCREEN... + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED), + ) + + // ...until STARTING back to LOCKSCREEN, at which point the "current" state should be + // the + // one we're transitioning from, despite never FINISHING in that state. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + AOD, + ), + currentStates + ) + + sendSteps( + TransitionStep(AOD, LOCKSCREEN, 0.8f, RUNNING), + TransitionStep(AOD, LOCKSCREEN, 0.8f, FINISHED), + ) + + // FINSHING in LOCKSCREEN should update the current state to LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + currentStates + ) + } + + @Test + fun testCurrentState_multipleCancellations_backToLastFinishedState() = + testScope.runTest { + val currentStates by collectValues(underTest.currentKeyguardState) + + // We init the repo with a transition from OFF -> LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + // Default transition from OFF -> LOCKSCREEN + OFF, + LOCKSCREEN, + // Transitioned to GONE + GONE, + ), + currentStates + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + TransitionStep(GONE, DOZING, 0.5f, RUNNING), + TransitionStep(GONE, DOZING, 0.6f, CANCELED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + // Current state should not be DOZING until the post-cancelation transition is + // STARTED + ), + currentStates + ) + + sendSteps( + TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + // DOZING -> LS STARTED, DOZING is now the current state. + DOZING, + ), + currentStates + ) + + sendSteps( + TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING), + TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + // LS -> GONE STARTED, LS is now the current state. + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + LOCKSCREEN, + // FINISHED in GONE, GONE is now the current state. + GONE, + ), + currentStates + ) + } private suspend fun sendSteps(vararg steps: TransitionStep) { steps.forEach { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 04e90c8deebf..aa15d0befa80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -21,11 +21,19 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -37,9 +45,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LockscreenSceneViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor private val underTest = createLockscreenSceneViewModel() @@ -47,9 +55,11 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_canSwipeToUnlock_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) @@ -59,8 +69,10 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) @@ -69,7 +81,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { @Test fun leftTransitionSceneKey_communalIsEnabled_communal() = testScope.runTest { - utils.communalRepository.setIsCommunalEnabled(true) + kosmos.fakeCommunalRepository.setIsCommunalEnabled(true) val underTest = createLockscreenSceneViewModel() assertThat(underTest.leftDestinationSceneKey).isEqualTo(SceneKey.Communal) @@ -78,7 +90,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { @Test fun leftTransitionSceneKey_communalIsDisabled_null() = testScope.runTest { - utils.communalRepository.setIsCommunalEnabled(false) + kosmos.fakeCommunalRepository.setIsCommunalEnabled(false) val underTest = createLockscreenSceneViewModel() assertThat(underTest.leftDestinationSceneKey).isNull() @@ -87,13 +99,13 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel { return LockscreenSceneViewModel( applicationScope = testScope.backgroundScope, - deviceEntryInteractor = utils.deviceEntryInteractor(), - communalInteractor = utils.communalInteractor(), + deviceEntryInteractor = kosmos.deviceEntryInteractor, + communalInteractor = kosmos.communalInteractor, longPress = KeyguardLongPressViewModel( interactor = mock(), ), - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index 6403ed19a9b2..be523b813494 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -22,13 +22,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.kosmos.testScope import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -47,9 +50,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QuickSettingsSceneViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() } @@ -90,7 +93,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { QuickSettingsSceneViewModel( shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index ecc2ef15dec8..1cd764e01bad 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -27,22 +27,38 @@ import com.android.internal.util.EmergencyAffordanceManager import com.android.internal.util.emergencyAffordanceManager import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel +import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.classifier.falsingCollector +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.model.SysUiState import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.SceneContainerStartable +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -50,13 +66,17 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -101,13 +121,13 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class SceneFrameworkIntegrationTest : SysuiTestCase() { - private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true } - private val testScope = utils.testScope - private val sceneContainerConfig = utils.fakeSceneContainerConfig() - private val sceneInteractor = utils.sceneInteractor() - private val authenticationInteractor = utils.authenticationInteractor() - private val deviceEntryInteractor = utils.deviceEntryInteractor() - private val communalInteractor = utils.communalInteractor() + private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } + private val testScope = kosmos.testScope + private val sceneContainerConfig = kosmos.sceneContainerConfig + private val sceneInteractor = kosmos.sceneInteractor + private val authenticationInteractor = kosmos.authenticationInteractor + private val deviceEntryInteractor = kosmos.deviceEntryInteractor + private val communalInteractor = kosmos.communalInteractor private val transitionState = MutableStateFlow<ObservableTransitionState>( @@ -116,11 +136,11 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private val sceneContainerViewModel = SceneContainerViewModel( sceneInteractor = sceneInteractor, - falsingInteractor = utils.falsingInteractor(), + falsingInteractor = kosmos.falsingInteractor, ) .apply { setTransitionState(transitionState) } - private val bouncerInteractor = utils.bouncerInteractor() + private val bouncerInteractor = kosmos.bouncerInteractor private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor @@ -135,7 +155,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { KeyguardLongPressViewModel( interactor = mock(), ), - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, ) private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) @@ -152,22 +172,21 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { FakeMobileConnectionsRepository(), ), constants = mock(), - utils.fakeFeatureFlags, + flags = kosmos.fakeFeatureFlagsClassic, scope = testScope.backgroundScope, ) private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel private lateinit var shadeSceneViewModel: ShadeSceneViewModel - private val keyguardInteractor = utils.keyguardInteractor() - private val powerInteractor = utils.powerInteractor() + private val keyguardInteractor = kosmos.keyguardInteractor + private val powerInteractor = kosmos.powerInteractor private var bouncerSceneJob: Job? = null private val qsFlexiglassAdapter = FakeQSSceneAdapter(inflateDelegate = { mock() }) @Mock private lateinit var mediaDataManager: MediaDataManager - @Mock private lateinit var mediaHost: MediaHost private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager private lateinit var telecomManager: TelecomManager @@ -177,27 +196,27 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, true) - telecomManager = checkNotNull(utils.kosmos.telecomManager) + telecomManager = checkNotNull(kosmos.telecomManager) whenever(telecomManager.isInCall).thenReturn(false) - emergencyAffordanceManager = utils.kosmos.emergencyAffordanceManager + emergencyAffordanceManager = kosmos.emergencyAffordanceManager whenever(emergencyAffordanceManager.needsEmergencyAffordance()).thenReturn(true) - utils.fakeFeatureFlags.apply { + kosmos.fakeFeatureFlagsClassic.apply { set(Flags.NEW_NETWORK_SLICE_UI, false) set(Flags.REFACTOR_GETCURRENTUSER, true) } - mobileConnectionsRepository = utils.mobileConnectionsRepository + mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository mobileConnectionsRepository.isAnySimSecure.value = false - utils.telephonyRepository.apply { + kosmos.fakeTelephonyRepository.apply { setHasTelephonyRadio(true) setCallState(TelephonyManager.CALL_STATE_IDLE) setIsInCall(false) } - bouncerActionButtonInteractor = utils.bouncerActionButtonInteractor() - bouncerViewModel = utils.bouncerViewModel() + bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor + bouncerViewModel = kosmos.bouncerViewModel shadeHeaderViewModel = ShadeHeaderViewModel( @@ -215,12 +234,11 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { deviceEntryInteractor = deviceEntryInteractor, shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, - mediaHost = mediaHost, ) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) val displayTracker = FakeDisplayTracker(context) val sysUiState = SysUiState(displayTracker) @@ -230,15 +248,15 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { sceneInteractor = sceneInteractor, deviceEntryInteractor = deviceEntryInteractor, keyguardInteractor = keyguardInteractor, - flags = utils.fakeSceneContainerFlags, + flags = kosmos.fakeSceneContainerFlags, sysUiState = sysUiState, displayId = displayTracker.defaultDisplayId, sceneLogger = mock(), - falsingCollector = utils.falsingCollector(), + falsingCollector = kosmos.falsingCollector, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, - simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, - authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() }, + simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, + authenticationInteractor = dagger.Lazy { kosmos.authenticationInteractor }, windowController = mock(), ) startable.start() @@ -534,15 +552,15 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { // Set the lockscreen enabled bit _before_ set the auth method as the code picks up on the // lockscreen enabled bit _after_ the auth method is changed and the lockscreen enabled bit // is not an observable that can trigger a new evaluation. - utils.deviceEntryRepository.setLockscreenEnabled(enableLockscreen) - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(enableLockscreen) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) runCurrent() } /** Emulates a phone call in progress. */ private fun TestScope.startPhoneCall() { whenever(telecomManager.isInCall).thenReturn(true) - utils.telephonyRepository.apply { + kosmos.fakeTelephonyRepository.apply { setHasTelephonyRadio(true) setIsInCall(true) setCallState(TelephonyManager.CALL_STATE_OFFHOOK) @@ -651,7 +669,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .that(authMethod.isSecure) .isTrue() - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() } @@ -665,7 +683,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { enterPin() // This repository state is not changed by the AuthInteractor, it relies on // KeyguardStateController. - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) emulateUiSceneTransition( expectedVisible = false, ) @@ -721,7 +739,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } pinBouncerViewModel.onAuthenticateButtonClicked() setAuthMethod(authMethodAfterSimUnlock) - utils.mobileConnectionsRepository.isAnySimSecure.value = false + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = false runCurrent() } @@ -768,7 +786,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun TestScope.introduceLockedSim() { setAuthMethod(AuthenticationMethodModel.Sim) - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true runCurrent() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 339d0263a089..b267720971cd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -22,11 +22,14 @@ 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.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.sceneKeys +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -39,12 +42,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SceneContainerRepositoryTest : SysuiTestCase() { - private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true } - private val testScope = utils.testScope + private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } + private val testScope = kosmos.testScope @Test fun allSceneKeys() { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository assertThat(underTest.allSceneKeys()) .isEqualTo( listOf( @@ -61,7 +64,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun desiredScene() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val currentScene by collectLastValue(underTest.desiredScene) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) @@ -71,15 +74,15 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun setDesiredScene_noSuchSceneInContainer_throws() { - utils.kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) - val underTest = utils.fakeSceneContainerRepository() + kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) + val underTest = kosmos.sceneContainerRepository underTest.setDesiredScene(SceneModel(SceneKey.Shade)) } @Test fun isVisible() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val isVisible by collectLastValue(underTest.isVisible) assertThat(isVisible).isTrue() @@ -93,19 +96,19 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun transitionState_defaultsToIdle() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val transitionState by collectLastValue(underTest.transitionState) assertThat(transitionState) .isEqualTo( - ObservableTransitionState.Idle(utils.fakeSceneContainerConfig().initialSceneKey) + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } @Test fun transitionState_reflectsUpdates() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) @@ -134,7 +137,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { underTest.setTransitionState(null) assertThat(reflectedTransitionState) .isEqualTo( - ObservableTransitionState.Idle(utils.fakeSceneContainerConfig().initialSceneKey) + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 486f7ab6501d..d159986015a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -20,10 +20,16 @@ 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.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.scene.data.repository.sceneContainerRepository +import com.android.systemui.scene.sceneContainerConfig +import com.android.systemui.scene.sceneKeys +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -36,20 +42,20 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SceneInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var underTest: SceneInteractor @Before fun setUp() { - utils.fakeSceneContainerFlags.enabled = true - underTest = utils.sceneInteractor() + kosmos.fakeSceneContainerFlags.enabled = true + underTest = kosmos.sceneInteractor } @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys()).isEqualTo(utils.fakeSceneKeys()) + assertThat(underTest.allSceneKeys()).isEqualTo(kosmos.sceneKeys) } @Test @@ -75,7 +81,7 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun transitionState() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) @@ -104,7 +110,7 @@ class SceneInteractorTest : SysuiTestCase() { underTest.setTransitionState(null) assertThat(reflectedTransitionState) .isEqualTo( - ObservableTransitionState.Idle(utils.fakeSceneContainerConfig().initialSceneKey) + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } @@ -348,8 +354,8 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun userInput() = testScope.runTest { - assertThat(utils.powerRepository.userTouchRegistered).isFalse() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() underTest.onUserInput() - assertThat(utils.powerRepository.userTouchRegistered).isTrue() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt index 8be4eeb7be7a..f23716ccca54 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.scene.domain.interactor +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService @@ -28,7 +30,11 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository import com.android.systemui.statusbar.NotificationPresenter +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.init.NotificationsController +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -37,6 +43,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -50,6 +57,7 @@ import org.mockito.Mockito.verify class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { private val testScope = TestScope() + private val testDispatcher = StandardTestDispatcher() private val iStatusBarService = mock<IStatusBarService>() private val executor = FakeExecutor(FakeSystemClock()) private val windowRootViewVisibilityRepository = @@ -59,6 +67,9 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { private val notificationPresenter = mock<NotificationPresenter>() private val notificationsController = mock<NotificationsController>() private val powerInteractor = PowerInteractorFactory.create().powerInteractor + private val activeNotificationsRepository = ActiveNotificationListRepository() + private val activeNotificationsInteractor = + ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher) private val underTest = WindowRootViewVisibilityInteractor( @@ -67,6 +78,7 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { keyguardRepository, headsUpManager, powerInteractor, + activeNotificationsInteractor, ) .apply { setUp(notificationPresenter, notificationsController) } @@ -257,7 +269,8 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test - fun lockscreenShadeInteractive_hasHeadsUpAndNotifPresenterCollapsed_notifCountOne() = + @DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_hasHeadsUpAndNotifPresenterCollapsed_flagOff_notifCountOne() = testScope.runTest { underTest.start() @@ -273,6 +286,23 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_hasHeadsUpAndNotifPresenterCollapsed_flagOn_notifCountOne() = + testScope.runTest { + underTest.start() + + whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(true) + whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(true) + activeNotificationsRepository.setActiveNotifs(4) + + makeLockscreenShadeVisible() + + val notifCount = argumentCaptor<Int>() + verify(iStatusBarService).onPanelRevealed(any(), notifCount.capture()) + assertThat(notifCount.value).isEqualTo(1) + } + + @Test fun lockscreenShadeInteractive_hasHeadsUpAndNullPresenter_notifCountOne() = testScope.runTest { underTest.start() @@ -288,7 +318,8 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test - fun lockscreenShadeInteractive_noHeadsUp_notifCountMatchesNotifController() = + @DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_noHeadsUp_flagOff_notifCountMatchesNotifController() = testScope.runTest { underTest.start() whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(true) @@ -304,7 +335,25 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test - fun lockscreenShadeInteractive_notifPresenterNotCollapsed_notifCountMatchesNotifController() = + @EnableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_noHeadsUp_flagOn_notifCountMatchesNotifController() = + testScope.runTest { + underTest.start() + whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(true) + + whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(false) + activeNotificationsRepository.setActiveNotifs(9) + + makeLockscreenShadeVisible() + + val notifCount = argumentCaptor<Int>() + verify(iStatusBarService).onPanelRevealed(any(), notifCount.capture()) + assertThat(notifCount.value).isEqualTo(9) + } + + @Test + @DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_notifPresenterNotCollapsed_flagOff_notifCountMatchesNotifController() = testScope.runTest { underTest.start() whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(true) @@ -320,6 +369,23 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_notifPresenterNotCollapsed_flagOn_notifCountMatchesNotifController() = + testScope.runTest { + underTest.start() + whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(true) + + whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(false) + activeNotificationsRepository.setActiveNotifs(8) + + makeLockscreenShadeVisible() + + val notifCount = argumentCaptor<Int>() + verify(iStatusBarService).onPanelRevealed(any(), notifCount.capture()) + assertThat(notifCount.value).isEqualTo(8) + } + + @Test fun lockscreenShadeInteractive_noHeadsUp_noNotifController_notifCountZero() = testScope.runTest { underTest.start() 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 5fe4ca1fbfe3..4afa5f2a44b9 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 @@ -25,19 +25,31 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags as AconfigFlags import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.testScope import com.android.systemui.model.SysUiState import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -65,15 +77,15 @@ class SceneContainerStartableTest : SysuiTestCase() { @Mock private lateinit var windowController: NotificationShadeWindowController - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() - private val sceneContainerFlags = utils.fakeSceneContainerFlags - private val authenticationInteractor = utils.authenticationInteractor() - private val bouncerInteractor = utils.bouncerInteractor() - private val faceAuthRepository = utils.kosmos.fakeDeviceEntryFaceAuthRepository - private val deviceEntryInteractor = utils.deviceEntryInteractor() - private val keyguardInteractor = utils.keyguardInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val sceneContainerFlags = kosmos.fakeSceneContainerFlags + private val authenticationInteractor = kosmos.authenticationInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + private val deviceEntryInteractor = kosmos.deviceEntryInteractor + private val keyguardInteractor = kosmos.keyguardInteractor private val sysUiState: SysUiState = mock() private val falsingCollector: FalsingCollector = mock() private val powerInteractor = PowerInteractorFactory.create().powerInteractor @@ -97,7 +109,7 @@ class SceneContainerStartableTest : SysuiTestCase() { falsingCollector = falsingCollector, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, - simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, + simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, authenticationInteractor = dagger.Lazy { authenticationInteractor }, windowController = windowController, ) @@ -172,7 +184,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) underTest.start() - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -188,7 +200,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) underTest.start() - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } @@ -204,7 +216,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } @@ -222,7 +234,7 @@ class SceneContainerStartableTest : SysuiTestCase() { // Authenticate using a passive auth method like face auth while bypass is disabled. faceAuthRepository.isAuthenticated.value = true - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -244,7 +256,7 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionStateFlowValue.value = ObservableTransitionState.Idle(SceneKey.Shade) assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) @@ -263,7 +275,7 @@ class SceneContainerStartableTest : SysuiTestCase() { // Authenticate using a passive auth method like face auth while bypass is disabled. faceAuthRepository.isAuthenticated.value = true - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } @@ -373,7 +385,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() powerInteractor.setAwakeForTest() runCurrent() @@ -463,11 +475,11 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(falsingCollector).setShowingAod(false) - utils.keyguardRepository.setIsDozing(true) + kosmos.fakeKeyguardRepository.setIsDozing(true) runCurrent() verify(falsingCollector).setShowingAod(true) - utils.keyguardRepository.setIsDozing(false) + kosmos.fakeKeyguardRepository.setIsDozing(false) runCurrent() verify(falsingCollector, times(2)).setShowingAod(false) } @@ -493,7 +505,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun collectFalsingSignals_screenOnAndOff_aodUnavailable() = testScope.runTest { - utils.keyguardRepository.setAodAvailable(false) + kosmos.fakeKeyguardRepository.setAodAvailable(false) runCurrent() prepareState( initialSceneKey = SceneKey.Lockscreen, @@ -541,7 +553,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun collectFalsingSignals_screenOnAndOff_aodAvailable() = testScope.runTest { - utils.keyguardRepository.setAodAvailable(true) + kosmos.fakeKeyguardRepository.setAodAvailable(true) runCurrent() prepareState( initialSceneKey = SceneKey.Lockscreen, @@ -619,7 +631,7 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() runCurrent() - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) @@ -628,7 +640,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchesToLockscreen_whenSimBecomesUnlocked() = testScope.runTest { - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( @@ -638,7 +650,7 @@ class SceneContainerStartableTest : SysuiTestCase() { ) underTest.start() runCurrent() - utils.mobileConnectionsRepository.isAnySimSecure.value = false + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = false runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) @@ -647,7 +659,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchesToGone_whenSimBecomesUnlocked_ifDeviceUnlockedAndLockscreenDisabled() = testScope.runTest { - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( @@ -658,7 +670,7 @@ class SceneContainerStartableTest : SysuiTestCase() { ) underTest.start() runCurrent() - utils.mobileConnectionsRepository.isAnySimSecure.value = false + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = false runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) @@ -730,8 +742,8 @@ class SceneContainerStartableTest : SysuiTestCase() { } } sceneContainerFlags.enabled = true - utils.deviceEntryRepository.setUnlocked(isDeviceUnlocked) - utils.deviceEntryRepository.setBypassEnabled(isBypassEnabled) + kosmos.fakeDeviceEntryRepository.setUnlocked(isDeviceUnlocked) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(isBypassEnabled) val transitionStateFlow = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) @@ -743,8 +755,8 @@ class SceneContainerStartableTest : SysuiTestCase() { sceneInteractor.onSceneChanged(SceneModel(it), "reason") } authenticationMethod?.let { - utils.authenticationRepository.setAuthenticationMethod(authenticationMethod) - utils.deviceEntryRepository.setLockscreenEnabled( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authenticationMethod) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled( isLockscreenEnabled = isLockscreenEnabled ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index 3a4ee642685f..ede453d85ee1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -21,10 +21,14 @@ package com.android.systemui.scene.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.sceneKeys +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -36,17 +40,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SceneContainerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val interactor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val interactor = kosmos.sceneInteractor private lateinit var underTest: SceneContainerViewModel @Before fun setUp() { - utils.fakeSceneContainerFlags.enabled = true + kosmos.fakeSceneContainerFlags.enabled = true underTest = SceneContainerViewModel( sceneInteractor = interactor, - falsingInteractor = utils.falsingInteractor(), + falsingInteractor = kosmos.falsingInteractor, ) } @@ -64,7 +68,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys).isEqualTo(utils.fakeSceneKeys()) + assertThat(underTest.allSceneKeys).isEqualTo(kosmos.sceneKeys) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 77ddf15c41ad..51745023c5be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -7,7 +7,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository @@ -18,6 +19,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow @@ -31,9 +33,9 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class ShadeHeaderViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index a8133a3a4de3..e9801652f060 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -19,16 +19,20 @@ package com.android.systemui.shade.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -36,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -53,10 +58,10 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class ShadeSceneViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() - private val deviceEntryInteractor = utils.deviceEntryInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val deviceEntryInteractor = kosmos.deviceEntryInteractor private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } @@ -84,7 +89,6 @@ class ShadeSceneViewModelTest : SysuiTestCase() { private lateinit var underTest: ShadeSceneViewModel @Mock private lateinit var mediaDataManager: MediaDataManager - @Mock private lateinit var mediaHost: MediaHost @Before fun setUp() { @@ -105,9 +109,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { deviceEntryInteractor = deviceEntryInteractor, shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, - mediaHost = mediaHost, ) } @@ -115,8 +118,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceLocked_lockScreen() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -125,8 +130,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @@ -135,8 +142,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") @@ -147,8 +156,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") @@ -159,8 +170,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() underTest.onContentClicked() @@ -172,8 +185,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() underTest.onContentClicked() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt index 262795fe0eb6..8e8e5106e93c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -24,12 +24,13 @@ 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.scene.SceneTestUtils +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.telephony.TelephonyListenerManager +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -39,7 +40,6 @@ import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class TelephonyRepositoryImplTest : SysuiTestCase() { @@ -47,8 +47,8 @@ class TelephonyRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var manager: TelephonyListenerManager @Mock private lateinit var telecomManager: TelecomManager - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var underTest: TelephonyRepositoryImpl @@ -61,7 +61,7 @@ class TelephonyRepositoryImplTest : SysuiTestCase() { TelephonyRepositoryImpl( applicationScope = testScope.backgroundScope, applicationContext = context, - backgroundDispatcher = utils.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, manager = manager, telecomManager = telecomManager, ) diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml new file mode 100644 index 000000000000..84b89ca68e65 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true"> + <shape android:shape="rectangle"> + <corners android:radius="16dp" /> + <!--By default this outline will not show hence 0 width. + width is set programmatically when needed and is gated by the flag: + com.android.systemui.Flags.pinInputFieldStyledFocusState--> + <stroke android:width="0dp" + android:color="@color/bouncer_password_focus_color" /> + </shape> + </item> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml index 66c54f2a668e..0b35559148af 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml @@ -23,7 +23,7 @@ android:layout_marginTop="@dimen/keyguard_lock_padding" android:importantForAccessibility="no" android:ellipsize="marquee" - android:focusable="true" + android:focusable="false" android:gravity="center" android:singleLine="true" /> </merge> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 0628c3e957b1..ddad1e3f8940 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -25,6 +25,12 @@ <!-- Maximum width of the sliding KeyguardSecurityContainer --> <dimen name="keyguard_security_width">420dp</dimen> + <!-- Width for the keyguard pin input field --> + <dimen name="keyguard_pin_field_width">292dp</dimen> + + <!-- Width for the keyguard pin input field --> + <dimen name="keyguard_pin_field_height">48dp</dimen> + <!-- Height of the sliding KeyguardSecurityContainer (includes 2x keyguard_security_view_top_margin) --> <dimen name="keyguard_security_height">420dp</dimen> diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml index 565ed1085fb9..f51e1098f333 100644 --- a/packages/SystemUI/res-keyguard/values/strings.xml +++ b/packages/SystemUI/res-keyguard/values/strings.xml @@ -62,10 +62,10 @@ <string name="keyguard_plugged_in_charging_slowly"><xliff:g id="percentage">%s</xliff:g> • Charging slowly</string> <!-- When the lock screen is showing and the phone plugged in, and the defend mode is triggered, say that charging is temporarily limited. --> - <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging optimized to protect battery</string> + <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging on hold to protect battery</string> <!-- When the lock screen is showing and the phone plugged in with incompatible charger. --> - <string name="keyguard_plugged_in_incompatible_charger"><xliff:g id="percentage">%s</xliff:g> • Issue with charging accessory</string> + <string name="keyguard_plugged_in_incompatible_charger"><xliff:g id="percentage">%s</xliff:g> • Check charging accessory</string> <!-- SIM messages --><skip /> <!-- When the user inserts a sim card from an unsupported network, it becomes network locked --> diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index 2cca9510417a..4789a229c4d0 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -76,6 +76,7 @@ </style> <style name="Widget.TextView.Password" parent="@android:style/Widget.TextView"> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:background">@drawable/bouncer_password_text_view_focused_background</item> <item name="android:gravity">center</item> <item name="android:textColor">?android:attr/textColorPrimary</item> </style> diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml deleted file mode 100644 index 045c19eb09dc..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> -</vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml deleted file mode 100644 index 5e012ab7edbd..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillAlpha="0.3" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillColor="#fff"/> -</vector> - diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml deleted file mode 100644 index d8a9a703260d..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" - android:fillColor="#fff"/> - <path - android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" - android:fillColor="#fff"/> -</vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml deleted file mode 100644 index dec9930959a0..000000000000 --- a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0" - > - <path - android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" - android:fillColor="#fff"/> - <path - android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" - android:fillColor="#fff"/> - <path - android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" - android:fillColor="#fff"/> - <path - android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" - android:fillColor="#fff"/> - <path - android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" - android:fillColor="#fff"/> - <path - android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" - android:fillColor="#fff"/> -</vector> diff --git a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml deleted file mode 100644 index ee4d05c3bda5..000000000000 --- a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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. - --> - -<!-- Base layout that provides a single bindable icon_view id image view --> -<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center_vertical" - > - - <ImageView - android:id="@+id/icon_view" - android:layout_height="@dimen/status_bar_bindable_icon_size" - android:layout_width="wrap_content" - android:layout_gravity="center_vertical" - android:padding="@dimen/status_bar_bindable_icon_padding" - android:scaleType="fitCenter" - /> - -</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView> diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 23fbb12f3036..10f71134c4cc 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -20,6 +20,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> + <ImageView + android:id="@+id/logo" + android:layout_width="@dimen/biometric_auth_icon_size" + android:layout_height="@dimen/biometric_auth_icon_size" + android:layout_gravity="center" + android:scaleType="fitXY"/> + <TextView android:id="@+id/title" android:layout_width="match_parent" diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml index 0534c6ee466f..a0f916c6827a 100644 --- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml +++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml @@ -17,6 +17,7 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/root" style="@style/Widget.SliceView.Panel" android:layout_width="wrap_content" @@ -53,17 +54,40 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_title" /> + <View + android:id="@+id/bluetooth_tile_dialog_progress_background" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintEnd_toEndOf="@id/bluetooth_tile_dialog_progress_animation" + app:layout_constraintStart_toStartOf="@id/bluetooth_tile_dialog_progress_animation" + app:layout_constraintTop_toTopOf="@id/bluetooth_tile_dialog_progress_animation" + app:layout_constraintBottom_toBottomOf="@id/bluetooth_tile_dialog_progress_animation" + android:background="?androidprv:attr/colorSurfaceVariant" /> + + <ProgressBar + android:id="@+id/bluetooth_tile_dialog_progress_animation" + android:layout_width="152dp" + android:layout_height="4dp" + android:layout_marginTop="16dp" + style="@style/TrimmedHorizontalProgressBar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle" + android:visibility="invisible" + android:indeterminate="true" /> + <androidx.core.widget.NestedScrollView android:id="@+id/scroll_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="21dp" + android:minHeight="145dp" android:fillViewport="true" app:layout_constrainedHeight="true" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle"> + app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_progress_animation"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/scroll_layout" @@ -81,7 +105,7 @@ android:paddingStart="36dp" android:text="@string/turn_on_bluetooth" android:clickable="false" - android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:textAppearance="@style/TextAppearance.Dialog.Title" android:textSize="16sp" app:layout_constraintEnd_toStartOf="@+id/bluetooth_toggle" app:layout_constraintStart_toStartOf="parent" @@ -90,8 +114,7 @@ <Switch android:id="@+id/bluetooth_toggle" android:layout_width="wrap_content" - android:layout_height="48dp" - android:paddingTop="10dp" + android:layout_height="64dp" android:gravity="start|center_vertical" android:paddingEnd="40dp" android:contentDescription="@string/turn_on_bluetooth" @@ -107,7 +130,6 @@ android:id="@+id/device_list" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 85b6e8dc12b3..5db9eee6a908 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -50,7 +50,10 @@ app:layout_constraintStart_toStartOf="@id/album_art" app:layout_constraintEnd_toEndOf="@id/album_art" app:layout_constraintTop_toTopOf="@id/album_art" - app:layout_constraintBottom_toBottomOf="@id/album_art" /> + app:layout_constraintBottom_toBottomOf="@id/album_art" + android:clipToOutline="true" + android:background="@drawable/qs_media_outline_layout_bg" + /> <com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView android:id="@+id/turbulence_noise_view" @@ -59,7 +62,10 @@ app:layout_constraintStart_toStartOf="@id/album_art" app:layout_constraintEnd_toEndOf="@id/album_art" app:layout_constraintTop_toTopOf="@id/album_art" - app:layout_constraintBottom_toBottomOf="@id/album_art" /> + app:layout_constraintBottom_toBottomOf="@id/album_art" + android:clipToOutline="true" + android:background="@drawable/qs_media_outline_layout_bg" + /> <!-- Guideline for output switcher --> <androidx.constraintlayout.widget.Guideline diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index 73874a08b0bd..b3f32a2863a8 100644 --- a/packages/SystemUI/res/layout/qs_footer_impl.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -19,7 +19,7 @@ <com.android.systemui.qs.QSFooterView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/qs_footer" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="@dimen/qs_footer_height" android:layout_marginStart="@dimen/qs_footer_margin" android:layout_marginEnd="@dimen/qs_footer_margin" android:layout_marginBottom="@dimen/qs_footers_margin_bottom" @@ -31,7 +31,7 @@ <LinearLayout android:layout_width="match_parent" - android:layout_height="@dimen/qs_footer_height" + android:layout_height="match_parent" android:layout_gravity="center_vertical"> <TextView diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index bcc3c83b4560..61a323d44dfc 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -93,6 +93,8 @@ <color name="qs_user_switcher_selected_avatar_icon_color">#202124</color> <!-- Color of background circle of user avatars in quick settings user switcher --> <color name="qs_user_switcher_avatar_background">#3C4043</color> + <!-- Color of border for keyguard password input when focused --> + <color name="bouncer_password_focus_color">@*android:color/system_secondary_dark</color> <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% --> diff --git a/packages/SystemUI/res/values-sw720dp-land/config.xml b/packages/SystemUI/res/values-sw720dp-land/config.xml index be34a48d1cd6..0955f678dfd4 100644 --- a/packages/SystemUI/res/values-sw720dp-land/config.xml +++ b/packages/SystemUI/res/values-sw720dp-land/config.xml @@ -24,9 +24,6 @@ <!-- The maximum number of tiles in the QuickQSPanel --> <integer name="quick_qs_panel_max_tiles">6</integer> - <!-- Whether to use the split 2-column notification shade --> - <bool name="config_use_split_notification_shade">true</bool> - <!-- The number of columns in the QuickSettings --> <integer name="quick_settings_num_columns">3</integer> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 8be1cc7282e3..3839dd98cdd3 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -56,6 +56,8 @@ <color name="kg_user_switcher_restricted_avatar_icon_color">@color/GM2_grey_600</color> <!-- Color of background circle of user avatars in keyguard user switcher --> <color name="user_avatar_color_bg">?android:attr/colorBackgroundFloating</color> + <!-- Color of border for keyguard password input when focused --> + <color name="bouncer_password_focus_color">@*android:color/system_secondary_light</color> <!-- Icon color for user avatars in user switcher quick settings --> <color name="qs_user_switcher_avatar_icon_color">#3C4043</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 9a4520c5fa52..ee2a1ceab2b7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -151,8 +151,6 @@ <dimen name="status_bar_icon_size_sp">@*android:dimen/status_bar_icon_size_sp</dimen> <!-- Original dp height of notification icons in the status bar --> <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen> - <dimen name="status_bar_bindable_icon_size">20sp</dimen> - <dimen name="status_bar_bindable_icon_padding">2sp</dimen> <!-- Default horizontal drawable padding for status bar icons. --> <dimen name="status_bar_horizontal_padding">2.5sp</dimen> @@ -899,6 +897,10 @@ <dimen name="communal_tutorial_indicator_padding">24dp</dimen> <dimen name="communal_tutorial_indicator_horizontal_offset">32dp</dimen> + <!-- Size of the maximum radius for the enforced rounded rectangles on communal hub. + Keep it the same as in Launcher--> + <dimen name="communal_enforced_rounded_corner_max_radius">16dp</dimen> + <!-- The width/height of the unlock icon view on keyguard. --> <dimen name="keyguard_lock_height">42dp</dimen> <dimen name="keyguard_lock_padding">20dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 6e035e8c8c36..ec4c7d5bf67e 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -249,6 +249,9 @@ --> <item type="id" name="tag_smartspace_view" /> + <!-- ID of the Scene Container root Composable view --> + <item type='id' name="scene_container_root_composable" /> + <!-- Tag set on the Compose implementation of the QS footer actions. --> <item type="id" name="tag_compose_qs_footer_actions" /> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index a5a545af641a..033f93b260ab 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -3,6 +3,7 @@ package com.android.keyguard; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; +import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -191,11 +192,11 @@ public class KeyguardClockSwitch extends RelativeLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - - mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); - mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); - mStatusArea = findViewById(R.id.keyguard_status_area); - + if (!migrateClocksToBlueprint()) { + mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); + mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); + mStatusArea = findViewById(R.id.keyguard_status_area); + } onConfigChanged(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 66f965aea76f..efd8f7f97ca3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -37,6 +37,7 @@ import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.log.BouncerLogger; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -210,6 +211,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private final FeatureFlags mFeatureFlags; private final SelectedUserInteractor mSelectedUserInteractor; private final UiEventLogger mUiEventLogger; + private final KeyboardRepository mKeyboardRepository; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -223,7 +225,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> DevicePostureController devicePostureController, KeyguardViewController keyguardViewController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + KeyboardRepository keyboardRepository) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -240,6 +243,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mFeatureFlags = featureFlags; mSelectedUserInteractor = selectedUserInteractor; mUiEventLogger = uiEventLogger; + mKeyboardRepository = keyboardRepository; } /** Create a new {@link KeyguardInputViewController}. */ @@ -268,19 +272,22 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, emergencyButtonController, mFalsingCollector, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, - mUiEventLogger); + mUiEventLogger, mKeyboardRepository + ); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, - emergencyButtonController, mFeatureFlags, mSelectedUserInteractor); + emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, + mKeyboardRepository); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, - emergencyButtonController, mFeatureFlags, mSelectedUserInteractor); + emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, + mKeyboardRepository); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index 36fe75f69a45..fcff0dbc0878 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -25,6 +25,7 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FO import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; +import static com.android.systemui.Flags.pinInputFieldStyledFocusState; import android.animation.Animator; import android.animation.AnimatorSet; @@ -168,7 +169,9 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView // Set selected property on so the view can send accessibility events. mPasswordEntry.setSelected(true); - mPasswordEntry.setDefaultFocusHighlightEnabled(false); + if (!pinInputFieldStyledFocusState()) { + mPasswordEntry.setDefaultFocusHighlightEnabled(false); + } mOkButton = findViewById(R.id.key_enter); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 376933dc5519..60dd5686c315 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -16,17 +16,25 @@ package com.android.keyguard; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import static com.android.systemui.Flags.pinInputFieldStyledFocusState; + +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; +import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnKeyListener; import android.view.View.OnTouchListener; +import android.view.ViewGroup; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -35,6 +43,7 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB private final LiftToActivateListener mLiftToActivateListener; private final FalsingCollector mFalsingCollector; + private final KeyboardRepository mKeyboardRepository; protected PasswordTextView mPasswordEntry; private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> { @@ -65,12 +74,14 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB EmergencyButtonController emergencyButtonController, FalsingCollector falsingCollector, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, + KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, emergencyButtonController, featureFlags, selectedUserInteractor); mLiftToActivateListener = liftToActivateListener; mFalsingCollector = falsingCollector; + mKeyboardRepository = keyboardRepository; mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); } @@ -120,6 +131,35 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB }); okButton.setOnHoverListener(mLiftToActivateListener); } + if (pinInputFieldStyledFocusState()) { + collectFlow(mPasswordEntry, mKeyboardRepository.isAnyKeyboardConnected(), + this::setKeyboardBasedFocusOutline); + + /** + * new UI Specs for PIN Input field have new dimensions go/pin-focus-states. + * However we want these changes behind a flag, and resource files cannot be flagged + * hence the dimension change in code. When the flags are removed these dimensions + * should be set in resources permanently and the code below removed. + */ + ViewGroup.LayoutParams layoutParams = mPasswordEntry.getLayoutParams(); + layoutParams.width = (int) getResources().getDimension( + R.dimen.keyguard_pin_field_width); + layoutParams.height = (int) getResources().getDimension( + R.dimen.keyguard_pin_field_height); + } + } + + private void setKeyboardBasedFocusOutline(boolean isAnyKeyboardConnected) { + StateListDrawable background = (StateListDrawable) mPasswordEntry.getBackground(); + GradientDrawable stateDrawable = (GradientDrawable) background.getStateDrawable(0); + int color = getResources().getColor(R.color.bouncer_password_focus_color); + if (!isAnyKeyboardConnected) { + stateDrawable.setStroke(0, color); + } else { + int strokeWidthInDP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, + getResources().getDisplayMetrics()); + stateDrawable.setStroke(strokeWidthInDP, color); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 2aab1f189263..b958f55bdf79 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -28,6 +28,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -58,12 +59,13 @@ public class KeyguardPinViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, EmergencyButtonController emergencyButtonController, FalsingCollector falsingCollector, - DevicePostureController postureController, - FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - UiEventLogger uiEventLogger) { + DevicePostureController postureController, FeatureFlags featureFlags, + SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, + KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor); + emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, + keyboardRepository); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 5729119a582a..1cdcbd06815f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -44,6 +44,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -93,10 +94,11 @@ public class KeyguardSimPinViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor); + emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, + keyboardRepository); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index 05fb5fa75e9e..f019d61a3ccc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -39,6 +39,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -90,10 +91,11 @@ public class KeyguardSimPukViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor); + emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, + keyboardRepository); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index d372f5a616b1..1758831203d6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -494,7 +494,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV boolean shouldBeCentered, boolean animate) { if (migrateClocksToBlueprint()) { - mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered); + mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); } else { mKeyguardClockSwitchController.setSplitShadeCentered( splitShadeEnabled && shouldBeCentered); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt index b15aaaf9a00e..e88aaf015f87 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt +++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt @@ -91,7 +91,7 @@ abstract class SystemUIAppComponentFactoryBase : AppComponentFactory() { return app } - @UsesReflection(KeepTarget(extendsClassConstant = SysUIComponent::class, methodName = "inject")) + @UsesReflection(KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject")) override fun instantiateProviderCompat(cl: ClassLoader, className: String): ContentProvider { val contentProvider = super.instantiateProviderCompat(cl, className) if (contentProvider is ContextInitializer) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt index 6483ae44d5ec..c7e5b645db5f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt @@ -19,16 +19,20 @@ package com.android.systemui.accessibility.data.repository import android.os.UserHandle import android.provider.Settings.Secure import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withContext /** Provides data related to color correction. */ @@ -45,22 +49,24 @@ class ColorCorrectionRepositoryImpl @Inject constructor( @Background private val bgCoroutineContext: CoroutineContext, + @Application private val scope: CoroutineScope, private val secureSettings: SecureSettings, ) : ColorCorrectionRepository { - companion object { - const val SETTING_NAME = Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED - const val DISABLED = 0 - const val ENABLED = 1 - } + private val userMap = mutableMapOf<Int, Flow<Boolean>>() override fun isEnabled(userHandle: UserHandle): Flow<Boolean> = - secureSettings - .observerFlow(userHandle.identifier, SETTING_NAME) - .onStart { emit(Unit) } - .map { secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED } - .distinctUntilChanged() - .flowOn(bgCoroutineContext) + userMap.getOrPut(userHandle.identifier) { + secureSettings + .observerFlow(userHandle.identifier, SETTING_NAME) + .onStart { emit(Unit) } + .map { + secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + } + .distinctUntilChanged() + .flowOn(bgCoroutineContext) + .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + } override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean = withContext(bgCoroutineContext) { @@ -70,4 +76,10 @@ constructor( userHandle.identifier ) } + + companion object { + private const val SETTING_NAME = Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED + private const val DISABLED = 0 + private const val ENABLED = 1 + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt index bbf10c509e50..419eada91f87 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt @@ -19,16 +19,20 @@ package com.android.systemui.accessibility.data.repository import android.os.UserHandle import android.provider.Settings.Secure import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withContext /** Provides data related to color inversion. */ @@ -45,16 +49,24 @@ class ColorInversionRepositoryImpl @Inject constructor( @Background private val bgCoroutineContext: CoroutineContext, + @Application private val scope: CoroutineScope, private val secureSettings: SecureSettings, ) : ColorInversionRepository { + private val userMap = mutableMapOf<Int, Flow<Boolean>>() + override fun isEnabled(userHandle: UserHandle): Flow<Boolean> = - secureSettings - .observerFlow(userHandle.identifier, SETTING_NAME) - .onStart { emit(Unit) } - .map { secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED } - .distinctUntilChanged() - .flowOn(bgCoroutineContext) + userMap.getOrPut(userHandle.identifier) { + secureSettings + .observerFlow(userHandle.identifier, SETTING_NAME) + .onStart { emit(Unit) } + .map { + secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + } + .distinctUntilChanged() + .flowOn(bgCoroutineContext) + .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + } override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean = withContext(bgCoroutineContext) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index ab23564a1df4..57e308ff16e8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -399,7 +399,8 @@ public class AuthContainerView extends LinearLayout config.mPromptInfo, config.mUserId, config.mOperationId, - new BiometricModalities(fpProps, faceProps)); + new BiometricModalities(fpProps, faceProps), + config.mOpPackageName); final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate( R.layout.biometric_prompt_layout, null, false); @@ -470,7 +471,8 @@ public class AuthContainerView extends LinearLayout mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); mPromptSelectorInteractorProvider.get().useCredentialsForAuthentication( - mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId); + mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId, + mConfig.mOpPackageName); final CredentialViewModel vm = mCredentialViewModelProvider.get(); vm.setAnimateContents(animateContents); ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java index 86802a5b58b0..02eae9cedf74 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java @@ -148,6 +148,12 @@ public class UdfpsDialogMeasureAdapter { || child.getId() == R.id.customized_view_container) { //skip description view and compute later continue; + } else if (child.getId() == R.id.logo) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index b35fbbc7bb32..ad7bb0e61178 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -55,6 +55,9 @@ interface PromptRepository { /** The kind of credential to use (biometric, pin, pattern, etc.). */ val kind: StateFlow<PromptKind> + /** The package name that the prompt is called from. */ + val opPackageName: StateFlow<String?> + /** * If explicit confirmation is required. * @@ -68,6 +71,7 @@ interface PromptRepository { userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, + opPackageName: String, ) /** Unset the prompt info. */ @@ -108,6 +112,9 @@ constructor( private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric()) override val kind = _kind.asStateFlow() + private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null) + override val opPackageName = _opPackageName.asStateFlow() + private val _faceSettings = _userId.map { id -> faceSettings.forUser(id) }.distinctUntilChanged() private val _faceSettingAlwaysRequireConfirmation = @@ -127,11 +134,13 @@ constructor( userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, + opPackageName: String, ) { _kind.value = kind _userId.value = userId _challenge.value = gatekeeperChallenge _promptInfo.value = promptInfo + _opPackageName.value = opPackageName } override fun unsetPrompt() { @@ -139,6 +148,7 @@ constructor( _userId.value = null _challenge.value = null _kind.value = PromptKind.Biometric() + _opPackageName.value = null } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt index ac4b717a23ec..359e2e703ee6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -115,12 +115,14 @@ constructor( @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) { biometricPromptRepository.setPrompt( promptInfo, userId, challenge, - kind.asBiometricPromptCredential() + kind.asBiometricPromptCredential(), + opPackageName, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 65a2c0a2490f..b3f95748ccdc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -76,6 +76,7 @@ interface PromptSelectorInteractor { userId: Int, challenge: Long, modalities: BiometricModalities, + opPackageName: String, ) /** Use credential-based authentication instead of biometrics. */ @@ -84,6 +85,7 @@ interface PromptSelectorInteractor { @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) /** Unset the current authentication request. */ @@ -104,9 +106,12 @@ constructor( promptRepository.promptInfo, promptRepository.challenge, promptRepository.userId, - promptRepository.kind - ) { promptInfo, challenge, userId, kind -> - if (promptInfo == null || userId == null || challenge == null) { + promptRepository.kind, + promptRepository.opPackageName, + ) { promptInfo, challenge, userId, kind, opPackageName -> + if ( + promptInfo == null || userId == null || challenge == null || opPackageName == null + ) { return@combine null } @@ -117,6 +122,7 @@ constructor( userInfo = BiometricUserInfo(userId = userId), operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge), modalities = kind.activeModalities, + opPackageName = opPackageName, ) else -> null } @@ -152,13 +158,15 @@ constructor( promptInfo: PromptInfo, userId: Int, challenge: Long, - modalities: BiometricModalities + modalities: BiometricModalities, + opPackageName: String, ) { promptRepository.setPrompt( promptInfo = promptInfo, userId = userId, gatekeeperChallenge = challenge, kind = PromptKind.Biometric(modalities), + opPackageName = opPackageName, ) } @@ -167,12 +175,14 @@ constructor( @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) { promptRepository.setPrompt( promptInfo = promptInfo, userId = userId, gatekeeperChallenge = challenge, kind = kind.asBiometricPromptCredential(), + opPackageName = opPackageName, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index 437793798567..c17c8dced668 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -1,5 +1,6 @@ package com.android.systemui.biometrics.domain.model +import android.graphics.Bitmap import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo import com.android.systemui.biometrics.shared.model.BiometricModalities @@ -26,6 +27,7 @@ sealed class BiometricPromptRequest( userInfo: BiometricUserInfo, operationInfo: BiometricOperationInfo, val modalities: BiometricModalities, + val opPackageName: String, ) : BiometricPromptRequest( title = info.title?.toString() ?: "", @@ -36,6 +38,8 @@ sealed class BiometricPromptRequest( showEmergencyCallButton = info.isShowEmergencyCallButton ) { val contentView: PromptContentView? = info.contentView + val logoRes: Int = info.logoRes + val logoBitmap: Bitmap? = info.logoBitmap val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java index 60b454e9670e..b450896729b7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java @@ -115,6 +115,12 @@ public class BiometricPromptLayout extends LinearLayout { MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.logo) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.biometric_icon) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index 22b02da5a7d5..16e7f05fae37 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -20,9 +20,9 @@ import android.content.Context import android.content.res.Resources import android.content.res.Resources.Theme import android.graphics.Paint -import android.hardware.biometrics.PromptContentListItem -import android.hardware.biometrics.PromptContentListItemBulletedText -import android.hardware.biometrics.PromptContentListItemPlainText +import android.hardware.biometrics.PromptContentItem +import android.hardware.biometrics.PromptContentItemBulletedText +import android.hardware.biometrics.PromptContentItemPlainText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptVerticalListContentView import android.text.SpannableString @@ -111,21 +111,21 @@ private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout { return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout } -private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn( +private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( resources: Resources, ): Boolean { val passedInText: CharSequence = when (this) { - is PromptContentListItemPlainText -> text - is PromptContentListItemBulletedText -> text + is PromptContentItemPlainText -> text + is PromptContentItemBulletedText -> text else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } when (this) { - is PromptContentListItemPlainText, - is PromptContentListItemBulletedText -> { + is PromptContentItemPlainText, + is PromptContentItemBulletedText -> { val dialogMargin = resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding) val halfDialogWidth = @@ -155,12 +155,12 @@ private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn( return numLines > maxLines } else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } } -private fun PromptContentListItem.toView( +private fun PromptContentItem.toView( resources: Resources, inflater: LayoutInflater, theme: Theme, @@ -171,10 +171,10 @@ private fun PromptContentListItem.toView( textView.layoutParams = lp when (this) { - is PromptContentListItemPlainText -> { + is PromptContentItemPlainText -> { textView.text = text } - is PromptContentListItemBulletedText -> { + is PromptContentItemBulletedText -> { val bulletedText = SpannableString(text) val span = BulletSpan( @@ -186,25 +186,25 @@ private fun PromptContentListItem.toView( textView.text = bulletedText } else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } return textView } -private fun PromptContentListItem.getListItemPadding(resources: Resources): Int { +private fun PromptContentItem.getListItemPadding(resources: Resources): Int { var listItemPadding = resources.getDimensionPixelSize( R.dimen.biometric_prompt_content_list_item_padding_horizontal ) * 2 when (this) { - is PromptContentListItemPlainText -> {} - is PromptContentListItemBulletedText -> { + is PromptContentItemPlainText -> {} + is PromptContentItemBulletedText -> { listItemPadding += getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources) } else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } return listItemPadding diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 04dc7a8da9f3..285ab4a800b6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -31,6 +31,7 @@ import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.accessibility.AccessibilityManager import android.widget.Button +import android.widget.ImageView import android.widget.ScrollView import android.widget.TextView import androidx.lifecycle.DefaultLifecycleObserver @@ -92,6 +93,7 @@ object BiometricViewBinder { val textColorHint = view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + val logoView = view.requireViewById<ImageView>(R.id.logo) val titleView = view.requireViewById<TextView>(R.id.title) val subtitleView = view.requireViewById<TextView>(R.id.subtitle) val descriptionView = view.requireViewById<TextView>(R.id.description) @@ -99,6 +101,8 @@ object BiometricViewBinder { view.requireViewById<ScrollView>(R.id.customized_view_container) // set selected to enable marquee unless a screen reader is enabled + logoView.isSelected = + !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled titleView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled subtitleView.isSelected = @@ -152,6 +156,7 @@ object BiometricViewBinder { } } + logoView.setImageDrawable(viewModel.logo.first()) titleView.text = viewModel.title.first() subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() @@ -183,6 +188,7 @@ object BiometricViewBinder { viewModel = viewModel, viewsToHideWhenSmall = listOf( + logoView, titleView, subtitleView, descriptionView, @@ -190,6 +196,7 @@ object BiometricViewBinder { ), viewsToFadeInOnSizeChange = listOf( + logoView, titleView, subtitleView, descriptionView, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index c3bbaedb2670..d5695f31f121 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.core.animation.addListener import androidx.core.view.doOnLayout @@ -234,7 +235,13 @@ private fun View.isLandscape(): Boolean { private fun View.showContentOrHide(forceHide: Boolean = false) { val isTextViewWithBlankText = this is TextView && this.text.isBlank() - visibility = if (forceHide || isTextViewWithBlankText) View.GONE else View.VISIBLE + val isImageViewWithoutImage = this is ImageView && this.drawable == null + visibility = + if (forceHide || isTextViewWithBlankText || isImageViewWithoutImage) { + View.GONE + } else { + View.VISIBLE + } } private fun View.asVerticalAnimator( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 1c789283ec70..dca0338dc8e7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -18,6 +18,8 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.PromptContentView import android.util.Log @@ -233,6 +235,19 @@ constructor( } } + /** Logo for the prompt. */ + val logo: Flow<Drawable?> = + promptSelectorInteractor.prompt + .map { + when { + it == null -> null + it.logoRes != -1 -> context.resources.getDrawable(it.logoRes, context.theme) + it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap) + else -> context.packageManager.getApplicationIcon(it.opPackageName) + } + } + .distinctUntilChanged() + /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() 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 10768ea6122a..dc07c1b25678 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.dagger import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule +import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule import com.android.systemui.communal.data.repository.CommunalRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule @@ -34,6 +35,7 @@ import dagger.Module CommunalTutorialRepositoryModule::class, CommunalWidgetRepositoryModule::class, CommunalDatabaseModule::class, + CommunalPrefsRepositoryModule::class, ] ) interface CommunalModule { 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 new file mode 100644 index 000000000000..c2ea2e93ce58 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -0,0 +1,108 @@ +/* + * 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 android.content.Context +import android.content.SharedPreferences +import android.content.pm.UserInfo +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserFileManagerExt.observeSharedPreferences +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Stores simple preferences for the current user in communal hub. For use cases like "has the CTA + * tile been dismissed?" + */ +interface CommunalPrefsRepository { + + /** Whether the CTA tile has been dismissed. */ + val isCtaDismissed: Flow<Boolean> + + /** Save the CTA tile dismissed state for the current user. */ + suspend fun setCtaDismissedForCurrentUser() +} + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class CommunalPrefsRepositoryImpl +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val userRepository: UserRepository, + private val userFileManager: UserFileManager, +) : CommunalPrefsRepository { + + override val isCtaDismissed: Flow<Boolean> = + userRepository.selectedUserInfo + .flatMapLatest(::observeCtaDismissState) + .stateIn( + scope = backgroundScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + + override suspend fun setCtaDismissedForCurrentUser() = + withContext(bgDispatcher) { + getSharedPrefsForUser(userRepository.getSelectedUserInfo()) + .edit() + .putBoolean(CTA_DISMISSED_STATE, true) + .apply() + } + + private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> = + userFileManager + .observeSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id) + // Emit at the start of collection to ensure we get an initial value + .onStart { emit(Unit) } + .map { getCtaDismissedState() } + .flowOn(bgDispatcher) + + private suspend fun getCtaDismissedState(): Boolean = + withContext(bgDispatcher) { + getSharedPrefsForUser(userRepository.getSelectedUserInfo()) + .getBoolean(CTA_DISMISSED_STATE, false) + } + + private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { + return userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + user.id, + ) + } + + companion object { + const val TAG = "CommunalRepository" + const val FILE_NAME = "communal_hub_prefs" + const val CTA_DISMISSED_STATE = "cta_dismissed" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryModule.kt new file mode 100644 index 000000000000..a4ff6d3dfbef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * 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 dagger.Binds +import dagger.Module + +@Module +interface CommunalPrefsRepositoryModule { + @Binds fun communalPrefsRepository(impl: CommunalPrefsRepositoryImpl): CommunalPrefsRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 553b3ebc0813..1f4be4060223 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -56,9 +56,6 @@ interface CommunalRepository { /** Exposes the transition state of the communal [SceneTransitionLayout]. */ val transitionState: StateFlow<ObservableCommunalTransitionState> - /** Whether the CTA tile is visible in the hub under view mode. */ - val isCtaTileInViewModeVisible: Flow<Boolean> - /** Updates the requested scene. */ fun setDesiredScene(desiredScene: CommunalSceneKey) @@ -68,9 +65,6 @@ interface CommunalRepository { * Note that you must call is with `null` when the UI is done or risk a memory leak. */ fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?) - - /** Updates whether to display the CTA tile in the hub under view mode. */ - fun setCtaTileInViewModeVisibility(isVisible: Boolean) } @OptIn(ExperimentalCoroutinesApi::class) @@ -102,16 +96,6 @@ constructor( initialValue = defaultTransitionState, ) - // TODO(b/313462210) - persist the value in local storage, so the tile won't show up again - // once dismissed. - private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) - override val isCtaTileInViewModeVisible: Flow<Boolean> = - _isCtaTileInViewModeVisible.asStateFlow() - - override fun setCtaTileInViewModeVisibility(isVisible: Boolean) { - _isCtaTileInViewModeVisible.value = isVisible - } - override fun setDesiredScene(desiredScene: CommunalSceneKey) { _desiredScene.value = desiredScene } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt index 9a9b0e29cbc4..046aaaa33342 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.data.repository import android.provider.Settings +import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED import android.provider.Settings.Secure.HubModeTutorialState import com.android.systemui.dagger.SysUISingleton @@ -24,7 +25,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.settings.UserTracker import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -35,6 +35,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -62,14 +63,19 @@ class CommunalTutorialRepositoryImpl constructor( @Application private val applicationScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, - userRepository: UserRepository, + private val userRepository: UserRepository, private val secureSettings: SecureSettings, - private val userTracker: UserTracker, @CommunalLog logBuffer: LogBuffer, ) : CommunalTutorialRepository { companion object { private const val TAG = "CommunalTutorialRepository" + + const val MIN_TUTORIAL_VERSION = HUB_MODE_TUTORIAL_COMPLETED + + // A version number which ensures that users, regardless of their completion of previous + // versions, see the updated tutorial when this number is bumped. + const val CURRENT_TUTORIAL_VERSION = MIN_TUTORIAL_VERSION + 1 } private data class SettingsState( @@ -80,7 +86,7 @@ constructor( private val settingsState: Flow<SettingsState> = userRepository.selectedUserInfo - .flatMapLatest { observeSettings() } + .flatMapLatest { userInfo -> observeSettings(userInfo.id) } .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed()) /** Emits the state of tutorial state in settings */ @@ -91,31 +97,37 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = HUB_MODE_TUTORIAL_NOT_STARTED + initialValue = HUB_MODE_TUTORIAL_NOT_STARTED, ) - private fun observeSettings(): Flow<SettingsState> = + private fun observeSettings(userId: Int): Flow<SettingsState> = secureSettings .observerFlow( - userId = userTracker.userId, - names = - arrayOf( - Settings.Secure.HUB_MODE_TUTORIAL_STATE, - ) + userId = userId, + names = arrayOf(Settings.Secure.HUB_MODE_TUTORIAL_STATE), ) // Force an update .onStart { emit(Unit) } - .map { readFromSettings() } + .map { readFromSettings(userId) } - private suspend fun readFromSettings(): SettingsState = + private suspend fun readFromSettings(userId: Int): SettingsState = withContext(backgroundDispatcher) { - val userId = userTracker.userId - val hubModeTutorialState = + var hubModeTutorialState = secureSettings.getIntForUser( Settings.Secure.HUB_MODE_TUTORIAL_STATE, HUB_MODE_TUTORIAL_NOT_STARTED, userId, ) + + if (hubModeTutorialState >= CURRENT_TUTORIAL_VERSION) { + // Tutorial is considered "completed" if the user has completed the current or a + // newer version. + hubModeTutorialState = HUB_MODE_TUTORIAL_COMPLETED + } else if (hubModeTutorialState >= MIN_TUTORIAL_VERSION) { + // Tutorial is considered "not started" if the user completed a version older than + // the current. + hubModeTutorialState = HUB_MODE_TUTORIAL_NOT_STARTED + } val settingsState = SettingsState(hubModeTutorialState) logger.d({ "Communal tutorial state for user $int1 in settings: $str1" }) { int1 = userId @@ -127,18 +139,40 @@ constructor( override suspend fun setTutorialState(state: Int): Unit = withContext(backgroundDispatcher) { - val userId = userTracker.userId + val userId = userRepository.getSelectedUserInfo().id if (tutorialSettingState.value == state) { return@withContext } + val newState = + if (state == HUB_MODE_TUTORIAL_COMPLETED) CURRENT_TUTORIAL_VERSION else state logger.d({ "Update communal tutorial state to $int1 for user $int2" }) { - int1 = state + int1 = newState int2 = userId } secureSettings.putIntForUser( Settings.Secure.HUB_MODE_TUTORIAL_STATE, - state, + newState, userId, ) } } + +// TODO(b/320769333): delete me and use the real repo above when tutorial is ready. +@SysUISingleton +class CommunalTutorialDisabledRepositoryImpl +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, +) : CommunalTutorialRepository { + override val tutorialSettingState: StateFlow<Int> = + emptyFlow<Int>() + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = HUB_MODE_TUTORIAL_COMPLETED, + ) + + override suspend fun setTutorialState(state: Int) { + // Do nothing + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryModule.kt index 69b0a27c55a8..b4f838ad5567 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryModule.kt @@ -22,6 +22,9 @@ import dagger.Module @Module interface CommunalTutorialRepositoryModule { + // TODO(b/320769333): use [CommunalTutorialRepositoryImpl] when tutorial is ready. @Binds - fun communalTutorialRepository(impl: CommunalTutorialRepositoryImpl): CommunalTutorialRepository + fun communalTutorialRepository( + impl: CommunalTutorialDisabledRepositoryImpl + ): CommunalTutorialRepository } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index e6816e954b5d..1c362e993509 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -16,26 +16,21 @@ package com.android.systemui.communal.data.repository -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.ComponentName -import android.content.Intent -import android.content.IntentFilter -import android.os.UserManager import androidx.annotation.WorkerThread -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.settings.UserTracker import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException @@ -43,16 +38,12 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext /** Encapsulates the state of widgets for communal mode. */ interface CommunalWidgetRepository { @@ -75,23 +66,21 @@ interface CommunalWidgetRepository { * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. */ fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} + + /** Update whether the app widget host should be active. */ + fun updateAppWidgetHostActive(active: Boolean) } -@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalWidgetRepositoryImpl @Inject constructor( private val appWidgetManager: Optional<AppWidgetManager>, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, @Application private val applicationScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, - broadcastDispatcher: BroadcastDispatcher, - communalRepository: CommunalRepository, private val communalWidgetHost: CommunalWidgetHost, private val communalWidgetDao: CommunalWidgetDao, - private val userManager: UserManager, - private val userTracker: UserTracker, @CommunalLog logBuffer: LogBuffer, ) : CommunalWidgetRepository { companion object { @@ -100,40 +89,22 @@ constructor( private val logger = Logger(logBuffer, TAG) - // Whether the [AppWidgetHost] is listening for updates. - private var isHostListening = false - - private suspend fun isUserUnlockingOrUnlocked(): Boolean = - withContext(bgDispatcher) { userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) } - - private val isUserUnlocked: Flow<Boolean> = - flowOf(communalRepository.isCommunalEnabled) - .flatMapLatest { enabled -> - if (enabled) { - broadcastDispatcher - .broadcastFlow( - filter = IntentFilter(Intent.ACTION_USER_UNLOCKED), - user = userTracker.userHandle - ) - .onStart { emit(Unit) } - .mapLatest { isUserUnlockingOrUnlocked() } - } else { - emptyFlow() - } - } - .distinctUntilChanged() - - private val isHostActive: Flow<Boolean> = - isUserUnlocked.map { - if (it) { - startListening() - true - } else { - stopListening() - false - } + override fun updateAppWidgetHostActive(active: Boolean) { + if (active == isHostActive.value) { + return + } + + if (active) { + appWidgetHost.startListening() + } else { + appWidgetHost.stopListening() } + isHostActive.value = active + } + + private val isHostActive = MutableStateFlow(false) + @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = isHostActive.flatMapLatest { isHostActive -> if (!isHostActive || !appWidgetManager.isPresent) { @@ -218,22 +189,4 @@ constructor( priority = entry.key.rank, ) } - - private fun startListening() { - if (isHostListening) { - return - } - - appWidgetHost.startListening() - isHostListening = true - } - - private fun stopListening() { - if (!isHostListening) { - return - } - - appWidgetHost.stopListening() - isHostListening = false - } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt index d0d9e3fabc7b..52f42c1aba4f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt @@ -17,11 +17,11 @@ package com.android.systemui.communal.data.repository -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.Context import android.content.res.Resources import com.android.systemui.communal.shared.CommunalWidgetHost +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -48,15 +48,15 @@ interface CommunalWidgetRepositoryModule { @SysUISingleton @Provides - fun provideAppWidgetHost(@Application context: Context): AppWidgetHost { - return AppWidgetHost(context, APP_WIDGET_HOST_ID) + fun provideCommunalAppWidgetHost(@Application context: Context): CommunalAppWidgetHost { + return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID) } @SysUISingleton @Provides fun provideCommunalWidgetHost( appWidgetManager: Optional<AppWidgetManager>, - appWidgetHost: AppWidgetHost, + appWidgetHost: CommunalAppWidgetHost, @CommunalLog logBuffer: LogBuffer, ): CommunalWidgetHost { return CommunalWidgetHost(appWidgetManager, appWidgetHost, logBuffer) 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 9fa4cd6c7985..71523b9e750f 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 @@ -17,9 +17,9 @@ package com.android.systemui.communal.domain.interactor import android.app.smartspace.SmartspaceTarget -import android.appwidget.AppWidgetHost import android.content.ComponentName import com.android.systemui.communal.data.repository.CommunalMediaRepository +import com.android.systemui.communal.data.repository.CommunalPrefsRepository import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel @@ -29,30 +29,40 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.HALF import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow 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 +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn /** Encapsulates business-logic related to communal mode. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalInteractor @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, + private val communalPrefsRepository: CommunalPrefsRepository, mediaRepository: CommunalMediaRepository, smartspaceRepository: SmartspaceRepository, keyguardInteractor: KeyguardInteractor, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { @@ -60,6 +70,20 @@ constructor( val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled + /** Whether communal features are enabled and available. */ + val isCommunalAvailable: StateFlow<Boolean> = + flowOf(isCommunalEnabled) + .flatMapLatest { enabled -> + if (enabled) keyguardInteractor.isEncryptedOrLockdown.map { !it } else flowOf(false) + } + .distinctUntilChanged() + .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + /** * Target scene as requested by the underlying [SceneTransitionLayout] or through * [onSceneChanged]. @@ -122,7 +146,7 @@ constructor( } /** Dismiss the CTA tile from the hub in view mode. */ - fun dismissCtaTile() = communalRepository.setCtaTileInViewModeVisibility(isVisible = false) + suspend fun dismissCtaTile() = communalPrefsRepository.setCtaDismissedForCurrentUser() /** * Add a widget at the specified position. @@ -174,8 +198,8 @@ constructor( /** CTA tile to be displayed in the glanceable hub (view mode). */ val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> = - communalRepository.isCtaTileInViewModeVisible.map { visible -> - if (visible) listOf(CommunalContentModel.CtaTileInViewMode()) else emptyList() + communalPrefsRepository.isCtaDismissed.map { isDismissed -> + if (isDismissed) emptyList() else listOf(CommunalContentModel.CtaTileInViewMode()) } /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 46f957f3aaf2..0d52afd4fff5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -16,10 +16,10 @@ package com.android.systemui.communal.domain.model -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetProviderInfo import android.widget.RemoteViews import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import java.util.UUID /** Encapsulates data for a communal content. */ @@ -44,7 +44,7 @@ sealed interface CommunalContentModel { class Widget( val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, - val appWidgetHost: AppWidgetHost, + val appWidgetHost: CommunalAppWidgetHost, ) : CommunalContentModel { override val key = KEY.widget(appWidgetId) // Widget size is always half. diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt index 41f9cb4c98ed..7fe37ccf0dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -16,11 +16,11 @@ package com.android.systemui.communal.shared -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog @@ -35,7 +35,7 @@ class CommunalWidgetHost @Inject constructor( private val appWidgetManager: Optional<AppWidgetManager>, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, @CommunalLog logBuffer: LogBuffer, ) { companion object { 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 84708a49f469..73bb0b0ec8a3 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 @@ -24,6 +24,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.media.controls.ui.MediaHost import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf @@ -32,10 +33,21 @@ abstract class BaseCommunalViewModel( private val communalInteractor: CommunalInteractor, val mediaHost: MediaHost, ) { + val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable + val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + /** Whether widgets are currently being re-ordered. */ + open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) + + private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null) + + /** The index of the currently selected item, or null if no item selected. */ + val selectedIndex: StateFlow<Int?> + get() = _selectedIndex + fun onSceneChanged(scene: CommunalSceneKey) { communalInteractor.onSceneChanged(scene) } @@ -105,4 +117,9 @@ abstract class BaseCommunalViewModel( /** Called as the user cancels dragging a widget to reorder. */ open fun onReorderWidgetCancel() {} + + /** Set the index of the currently selected item */ + fun setSelectedIndex(index: Int?) { + _selectedIndex.value = index + } } 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 7faf653cc177..317dd4040e7c 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 @@ -20,7 +20,6 @@ import android.app.Activity import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.app.ActivityOptions -import android.appwidget.AppWidgetHost import android.content.ActivityNotFoundException import android.content.ComponentName import android.widget.RemoteViews @@ -28,6 +27,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule @@ -37,7 +37,10 @@ import javax.inject.Named import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -45,7 +48,7 @@ class CommunalEditModeViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { @@ -69,9 +72,15 @@ constructor( // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent.map { widgets -> - widgets + listOf(CommunalContentModel.CtaTileInEditMode()) - } + communalInteractor.widgetContent + // Clear the selected index when the list is updated. + .onEach { setSelectedIndex(null) } + .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + + private val _reorderingWidgets = MutableStateFlow(false) + + override val reorderingWidgets: StateFlow<Boolean> + get() = _reorderingWidgets override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) @@ -135,14 +144,19 @@ constructor( } override fun onReorderWidgetStart() { + // Clear selection status + setSelectedIndex(null) + _reorderingWidgets.value = true uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) } override fun onReorderWidgetEnd() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) } override fun onReorderWidgetCancel() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 09c18ed0c8de..d619362b0311 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -86,9 +86,11 @@ constructor( override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() override fun onDismissCtaTile() { - communalInteractor.dismissCtaTile() - setPopupOnDismissCtaVisibility(true) - schedulePopupHiding() + scope.launch { + communalInteractor.dismissCtaTile() + setPopupOnDismissCtaVisibility(true) + schedulePopupHiding() + } } override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt new file mode 100644 index 000000000000..003c9d50e789 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context + +/** Communal app widget host that creates a [CommunalAppWidgetHostView]. */ +class CommunalAppWidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) { + override fun onCreateView( + context: Context, + appWidgetId: Int, + appWidget: AppWidgetProviderInfo? + ): AppWidgetHostView { + return CommunalAppWidgetHostView(context) + } + + /** + * Creates and returns a [CommunalAppWidgetHostView]. This method does the same thing as + * `createView`. The only difference is that the returned value will be casted to + * [CommunalAppWidgetHostView]. + */ + fun createViewForCommunal( + context: Context?, + appWidgetId: Int, + appWidget: AppWidgetProviderInfo? + ): CommunalAppWidgetHostView { + // `createView` internally calls `onCreateView` to create the view. We cannot override + // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView` + return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt new file mode 100644 index 000000000000..2b7d82395b89 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt @@ -0,0 +1,76 @@ +/* + * 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.widgets + +import android.appwidget.AppWidgetHostView +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.view.View +import android.view.ViewOutlineProvider + +/** AppWidgetHostView that displays in communal hub with support for rounded corners. */ +class CommunalAppWidgetHostView(context: Context) : AppWidgetHostView(context) { + // Mutable corner radius. + var enforcedCornerRadius: Float + + // Mutable `Rect`. The size will be mutated when the widget is reapplied. + var enforcedRectangle: Rect + + init { + enforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context) + enforcedRectangle = Rect() + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + + enforceRoundedCorners() + } + + private val cornerRadiusEnforcementOutline: ViewOutlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline) { + if (enforcedRectangle.isEmpty || enforcedCornerRadius <= 0) { + outline.setEmpty() + } else { + outline.setRoundRect(enforcedRectangle, enforcedCornerRadius) + } + } + } + + private fun enforceRoundedCorners() { + if (enforcedCornerRadius <= 0) { + resetRoundedCorners() + return + } + val background: View? = RoundedCornerEnforcement.findBackground(this) + if (background == null || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) { + resetRoundedCorners() + return + } + RoundedCornerEnforcement.computeRoundedRectangle(this, background, enforcedRectangle) + outlineProvider = cornerRadiusEnforcementOutline + clipToOutline = true + invalidateOutline() + } + + private fun resetRoundedCorners() { + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/RoundedCornerEnforcement.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/RoundedCornerEnforcement.kt new file mode 100644 index 000000000000..abda44be09fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/RoundedCornerEnforcement.kt @@ -0,0 +1,154 @@ +/* + * 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.widgets + +import android.annotation.IdRes +import android.annotation.Nullable +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import androidx.core.os.BuildCompat.isAtLeastS +import com.android.systemui.res.R +import kotlin.math.min + +/** + * Utilities to compute the enforced use of rounded corners on App Widgets. This is a fork of the + * Launcher3 source code to enforce the same visual treatment on communal hub. + */ +internal object RoundedCornerEnforcement { + /** + * Find the background view for a widget. + * + * @param appWidget the view containing the App Widget (typically the instance of + * [CommunalAppWidgetHostView]). + */ + fun findBackground(appWidget: View): View? { + val backgrounds = findViewsWithId(appWidget, R.id.background) + if (backgrounds.size == 1) { + return backgrounds[0] + } + // Really, the argument should contain the widget, so it cannot be the background. + if (appWidget is ViewGroup) { + val vg = appWidget + if (vg.childCount > 0) { + return findUndefinedBackground(vg.getChildAt(0)) + } + } + return appWidget + } + + /** Check whether the app widget has opted out of the enforcement. */ + fun hasAppWidgetOptedOut(appWidget: View?, background: View): Boolean { + return background.id == R.id.background && background.clipToOutline + } + + /** + * Computes the rounded rectangle needed for this app widget. + * + * @param appWidget View onto which the rounded rectangle will be applied. + * @param background Background view. This must be either `appWidget` or a descendant of + * `appWidget`. + * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame of + * `appWidget`. + */ + fun computeRoundedRectangle(appWidget: View, background: View, outRect: Rect) { + var background = background + outRect.left = 0 + outRect.right = background.width + outRect.top = 0 + outRect.bottom = background.height + while (background !== appWidget) { + outRect.offset(background.left, background.top) + background = background.parent as View + } + } + + /** Get the radius of the rounded rectangle defined in the host's resource. */ + private fun getOwnedEnforcedRadius(context: Context): Float { + val res: Resources = context.resources + return res.getDimension(R.dimen.communal_enforced_rounded_corner_max_radius) + } + + /** + * Computes the radius of the rounded rectangle that should be applied to a widget expanded in + * the given context. + */ + fun computeEnforcedRadius(context: Context): Float { + if (!isAtLeastS()) { + return 0f + } + val res: Resources = context.resources + val systemRadius: Float = + res.getDimension(android.R.dimen.system_app_widget_background_radius) + val defaultRadius = getOwnedEnforcedRadius(context) + return min(defaultRadius, systemRadius) + } + + private fun findViewsWithId(view: View, @IdRes viewId: Int): List<View> { + val output: MutableList<View> = ArrayList() + accumulateViewsWithId(view, viewId, output) + return output + } + + // Traverse views. If the predicate returns true, continue on the children, otherwise, don't. + private fun accumulateViewsWithId(view: View, @IdRes viewId: Int, output: MutableList<View>) { + if (view.id == viewId) { + output.add(view) + return + } + if (view is ViewGroup) { + val vg = view + for (i in 0 until vg.childCount) { + accumulateViewsWithId(vg.getChildAt(i), viewId, output) + } + } + } + + private fun isViewVisible(view: View): Boolean { + return if (view.visibility != View.VISIBLE) { + false + } else !view.willNotDraw() || view.foreground != null || view.background != null + } + + @Nullable + private fun findUndefinedBackground(current: View): View? { + if (current.visibility != View.VISIBLE) { + return null + } + if (isViewVisible(current)) { + return current + } + var lastVisibleView: View? = null + // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw + // something, or a ViewGroup that contains more than one view. + if (current is ViewGroup) { + val vg = current + for (i in 0 until vg.childCount) { + val visibleView = findUndefinedBackground(vg.getChildAt(i)) + if (visibleView != null) { + if (lastVisibleView != null) { + return current // At least two visible children + } + lastVisibleView = visibleView + } + } + } + return lastVisibleView + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index b2d70523c282..6d9994fb2205 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -556,7 +556,7 @@ public class FrameworkServicesModule { @Provides @Singleton static SubscriptionManager provideSubscriptionManager(Context context) { - return context.getSystemService(SubscriptionManager.class); + return context.getSystemService(SubscriptionManager.class).createForAllUserProfiles(); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt index 496c64e1120e..c6fb4f9d6956 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt @@ -19,6 +19,8 @@ package com.android.systemui.keyboard import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl import dagger.Binds import dagger.Module @@ -27,4 +29,9 @@ abstract class KeyboardModule { @Binds abstract fun bindKeyboardRepository(repository: KeyboardRepositoryImpl): KeyboardRepository + + @Binds + abstract fun bindStickyKeysRepository( + repository: StickyKeysRepositoryImpl + ): StickyKeysRepository } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt new file mode 100644 index 000000000000..37034f63aca7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.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.keyboard.stickykeys + +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.KeyboardLog +import javax.inject.Inject + +private const val TAG = "stickyKeys" + +class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) { + fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { str1 = linkedHashMap.toString() }, + { "new sticky keys state received: $str1" } + ) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt new file mode 100644 index 000000000000..34d288815570 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt @@ -0,0 +1,92 @@ +/* + * 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.stickykeys.data.repository + +import android.hardware.input.InputManager +import android.hardware.input.InputManager.StickyModifierStateListener +import android.hardware.input.StickyModifierState +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +interface StickyKeysRepository { + val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> + val settingEnabled: Flow<Boolean> +} + +class StickyKeysRepositoryImpl +@Inject +constructor( + private val inputManager: InputManager, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val stickyKeysLogger: StickyKeysLogger, +) : StickyKeysRepository { + + override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = + conflatedCallbackFlow { + val listener = StickyModifierStateListener { stickyModifierState -> + trySendWithFailureLogging(stickyModifierState, TAG) + } + // after registering, InputManager calls listener with the current value + inputManager.registerStickyModifierStateListener(Runnable::run, listener) + awaitClose { inputManager.unregisterStickyModifierStateListener(listener) } + } + .map { toStickyKeysMap(it) } + .onEach { stickyKeysLogger.logNewStickyKeysReceived(it) } + .flowOn(backgroundDispatcher) + + // TODO(b/319837892): Implement reading actual setting + override val settingEnabled: StateFlow<Boolean> = MutableStateFlow(true) + + private fun toStickyKeysMap(state: StickyModifierState): LinkedHashMap<ModifierKey, Locked> { + val keys = linkedMapOf<ModifierKey, Locked>() + state.apply { + if (isAltGrModifierOn) keys[ALT_GR] = Locked(false) + if (isAltGrModifierLocked) keys[ALT_GR] = Locked(true) + if (isAltModifierOn) keys[ALT] = Locked(false) + if (isAltModifierLocked) keys[ALT] = Locked(true) + if (isCtrlModifierOn) keys[CTRL] = Locked(false) + if (isCtrlModifierLocked) keys[CTRL] = Locked(true) + if (isMetaModifierOn) keys[META] = Locked(false) + if (isMetaModifierLocked) keys[META] = Locked(true) + if (isShiftModifierOn) keys[SHIFT] = Locked(false) + if (isShiftModifierLocked) keys[SHIFT] = Locked(true) + } + return keys + } + + companion object { + const val TAG = "StickyKeysRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt new file mode 100644 index 000000000000..d5f082a2566f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.shared.model + +@JvmInline +value class Locked(val locked: Boolean) + +enum class ModifierKey(val text: String) { + ALT("ALT LEFT"), + ALT_GR("ALT RIGHT"), + CTRL("CTRL"), + META("META"), + SHIFT("SHIFT"), +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt new file mode 100644 index 000000000000..26eb706da200 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.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.keyboard.stickykeys.ui.viewmodel + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +class StickyKeysIndicatorViewModel +@Inject +constructor( + stickyKeysRepository: StickyKeysRepository, + keyboardRepository: KeyboardRepository, + @Application applicationScope: CoroutineScope, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + val indicatorContent: Flow<Map<ModifierKey, Locked>> = + keyboardRepository.isAnyKeyboardConnected + .flatMapLatest { keyboardPresent -> + if (keyboardPresent) stickyKeysRepository.settingEnabled else flowOf(false) + } + .flatMapLatest { enabled -> + if (enabled) stickyKeysRepository.stickyKeys else flowOf(emptyMap()) + } + .stateIn(applicationScope, SharingStarted.Lazily, emptyMap()) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt index b373f8520254..fede47957a7b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt @@ -53,14 +53,14 @@ interface KeyguardFaceAuthModule { @SysUISingleton @FaceAuthTableLog fun provideFaceAuthTableLog(factory: TableLogBufferFactory): TableLogBuffer { - return factory.create("FaceAuthTableLog", 100) + return factory.create("FaceAuthTableLog", 400) } @Provides @SysUISingleton @FaceDetectTableLog fun provideFaceDetectTableLog(factory: TableLogBufferFactory): TableLogBuffer { - return factory.create("FaceDetectTableLog", 100) + return factory.create("FaceDetectTableLog", 400) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 704ebdd40af6..d012d24b767e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -40,11 +40,13 @@ import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -54,7 +56,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Defines interface for classes that encapsulate application state for the keyguard. */ @@ -208,6 +213,12 @@ interface KeyguardRepository { val clockShouldBeCentered: Flow<Boolean> /** + * Whether the primary authentication is required for the given user due to lockdown or + * encryption after reboot. + */ + val isEncryptedOrLockdown: Flow<Boolean> + + /** * Returns `true` if the keyguard is showing; `false` otherwise. * * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in @@ -279,6 +290,7 @@ constructor( @Application private val scope: CoroutineScope, private val systemClock: SystemClock, facePropertyRepository: FacePropertyRepository, + private val userTracker: UserTracker, ) : KeyguardRepository { private val _dismissAction: MutableStateFlow<DismissAction> = MutableStateFlow(DismissAction.None) @@ -544,6 +556,24 @@ constructor( awaitClose { dozeTransitionListener.removeCallback(callback) } } + @OptIn(ExperimentalCoroutinesApi::class) + override val isEncryptedOrLockdown: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onStrongAuthStateChanged(userId: Int) { + trySend(userId) + } + } + + keyguardUpdateMonitor.registerCallback(callback) + + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } + .filter { userId -> userId == userTracker.userId } + .onStart { emit(userTracker.userId) } + .mapLatest { userId -> keyguardUpdateMonitor.isEncryptedOrLockdown(userId) } + override fun isKeyguardShowing(): Boolean { return keyguardStateController.isShowing } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt new file mode 100644 index 000000000000..afe9151ac7a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt @@ -0,0 +1,34 @@ +/* + * 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.keyguard.data.repository + +import android.view.View +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class KeyguardSmartspaceRepository @Inject constructor() { + private val _bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(View.GONE) + val bcSmartspaceVisibility: StateFlow<Int> = _bcSmartspaceVisibility.asStateFlow() + + fun setBcSmartspaceVisibility(visibility: Int) { + _bcSmartspaceVisibility.value = visibility + } +} 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 fedd63be1454..8fa33ee7d0ca 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 @@ -54,12 +54,33 @@ constructor( ) { override fun start() { - listenForAodToLockscreenOrOccluded() + listenForAodToLockscreen() + listenForAodToPrimaryBouncer() listenForAodToGone() + listenForAodToOccluded() listenForTransitionToCamera(scope, keyguardInteractor) } - private fun listenForAodToLockscreenOrOccluded() { + /** + * There are cases where the transition to AOD begins but never completes, such as tapping power + * during an incoming phone call when unlocked. In this case, GONE->AOD should be interrupted to + * run AOD->OCCLUDED. + */ + private fun listenForAodToOccluded() { + scope.launch { + keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect { + (isOccluded, startedKeyguardState) -> + if (isOccluded && startedKeyguardState == KeyguardState.AOD) { + startTransitionTo( + toState = KeyguardState.OCCLUDED, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + } + } + } + } + + private fun listenForAodToLockscreen() { scope.launch { keyguardInteractor .dozeTransitionTo(DozeStateModel.FINISH) @@ -72,20 +93,15 @@ constructor( ::toTriple ) .collect { (_, lastStartedStep, occluded) -> - if (lastStartedStep.to == KeyguardState.AOD) { - val toState = - if (occluded) KeyguardState.OCCLUDED else KeyguardState.LOCKSCREEN + if (lastStartedStep.to == KeyguardState.AOD && !occluded) { val modeOnCanceled = - if ( - toState == KeyguardState.LOCKSCREEN && - lastStartedStep.from == KeyguardState.LOCKSCREEN - ) { + if (lastStartedStep.from == KeyguardState.LOCKSCREEN) { TransitionModeOnCanceled.REVERSE } else { TransitionModeOnCanceled.LAST_VALUE } startTransitionTo( - toState = toState, + toState = KeyguardState.LOCKSCREEN, modeOnCanceled = modeOnCanceled, ) } @@ -93,11 +109,26 @@ constructor( } } + /** + * If there is a biometric lockout and FPS is tapped while on AOD, it should go directly to the + * PRIMARY_BOUNCER. + */ + private fun listenForAodToPrimaryBouncer() { + scope.launch { + keyguardInteractor.primaryBouncerShowing + .sample(startedKeyguardTransitionStep, ::Pair) + .collect { (isBouncerShowing, lastStartedTransitionStep) -> + if (isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.AOD) { + startTransitionTo(KeyguardState.PRIMARY_BOUNCER) + } + } + } + } + private fun listenForAodToGone() { scope.launch { keyguardInteractor.biometricUnlockState.sample(finishedKeyguardState, ::Pair).collect { - pair -> - val (biometricUnlockState, keyguardState) = pair + (biometricUnlockState, keyguardState) -> if (keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)) { startTransitionTo(KeyguardState.GONE) } 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 6eb3b64d4c09..6170356d4a90 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 @@ -264,6 +264,12 @@ constructor( } } + /** + * Whether the primary authentication is required for the given user due to lockdown or + * encryption after reboot. + */ + val isEncryptedOrLockdown: Flow<Boolean> = repository.isEncryptedOrLockdown + fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> { return dozeTransitionModel.filter { states.contains(it.to) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt new file mode 100644 index 000000000000..67b57456a5c6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt @@ -0,0 +1,33 @@ +/* + * 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.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardSmartspaceRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +@SysUISingleton +class KeyguardSmartspaceInteractor +@Inject +constructor(private val keyguardSmartspaceRepository: KeyguardSmartspaceRepository) { + var bcSmartspaceVisibility: StateFlow<Int> = keyguardSmartspaceRepository.bcSmartspaceVisibility + + fun setBcSmartspaceVisibility(visibility: Int) { + keyguardSmartspaceRepository.setBcSmartspaceVisibility(visibility) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index a8223ea83e1f..b43ab5e9110d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -37,17 +37,19 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn /** Encapsulates business-logic related to the keyguard transitions. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardTransitionInteractor @Inject @@ -192,29 +194,121 @@ constructor( val finishedKeyguardTransitionStep: Flow<TransitionStep> = repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } - /** The destination state of the last started transition. */ + /** The destination state of the last [TransitionState.STARTED] transition. */ val startedKeyguardState: SharedFlow<KeyguardState> = startedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) - /** The last completed [KeyguardState] transition */ + /** + * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. + * + * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a + * value when a subsequent transition is STARTED. It will *only* emit once we have finally + * FINISHED in a state. This can have unintuitive implications. + * + * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in + * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain + * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition + * finishes (at which point we'll be FINISHED in LOCKSCREEN). + * + * Since there's no real limit to how many consecutive transitions can be canceled, it's even + * possible for the FINISHED state to be the same as the STARTED state while still + * transitioning. + * + * For example: + * 1. We're finished in GONE. + * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still + * FINISHED in GONE. + * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING -> + * LOCKSCREEN transition. We're still FINISHED in GONE. + * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this + * starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also + * STARTED a transition *to* GONE. + * 5. We'll emit KeyguardState.GONE again once the transition finishes. + * + * If you just need to know when we eventually settle into a state, this flow is likely + * sufficient. However, if you're having issues with state *during* transitions started after + * one or more canceled transitions, you probably need to use [currentKeyguardState]. + */ val finishedKeyguardState: SharedFlow<KeyguardState> = finishedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) /** - * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed - * it. + * The [KeyguardState] we're currently in. + * + * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in + * transition, this is the state we're transitioning *from*. + * + * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always + * identical - if a transition FINISHES in a given state, the subsequent state we START a + * transition *from* would always be that same previously FINISHED state. + * + * However, if a transition is CANCELED, the next transition will START from a state we never + * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in + * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never + * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still + * be GONE. + * + * In this example, if there was DOZING-related state that needs to be set up in order to + * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were + * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would + * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state. + * + * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your + * specific use case and how you want to handle cancellations. In general, if you're dealing + * with state/UI present across multiple [KeyguardState]s, you probably want + * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state, + * you likely want [finishedKeyguardState]. + * + * As an example, let's say you want to animate in a message on the lockscreen UI after waking + * up, and that TextView is not involved in animations between states. You'd want to collect + * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen. + * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is + * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible + * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in + * that case. That's likely not what you want. + * + * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during + * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE + * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation. + * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is + * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this + * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace + * during the LS -> GONE transition. + * + * If you need special-case handling for cancellations (such as conditional handling depending + * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep] + * directly. + * + * As a helpful footnote, here's the values of [finishedKeyguardState] and + * [currentKeyguardState] during a sequence with two cancellations: + * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE. + * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE; + * finishedKeyguardState=GONE. + * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN. + * currentKeyguardState=DOZING; finishedKeyguardState=GONE. + * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE. + * currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE. + * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE; + * finishedKeyguardState=GONE. */ - val isInTransitionToAnyState = - combine( - startedKeyguardTransitionStep, - finishedKeyguardState, - ) { startedStep, finishedState -> - startedStep.to != finishedState - } + val currentKeyguardState: SharedFlow<KeyguardState> = + repository.transitions + .mapLatest { + if (it.transitionState == TransitionState.FINISHED) { + it.to + } else { + it.from + } + } + .distinctUntilChanged() + .shareIn(scope, SharingStarted.Eagerly, replay = 1) + + /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */ + val isInTransitionToAnyState = isInTransitionWhere({ true }, { true }) /** * The amount of transition into or out of the given [KeyguardState]. @@ -304,13 +398,12 @@ constructor( fromStatePredicate: (KeyguardState) -> Boolean, toStatePredicate: (KeyguardState) -> Boolean, ): Flow<Boolean> { - return combine( - startedKeyguardTransitionStep, - finishedKeyguardState, - ) { startedStep, finishedState -> - fromStatePredicate(startedStep.from) && - toStatePredicate(startedStep.to) && - finishedState != startedStep.to + return repository.transitions + .filter { it.transitionState != TransitionState.CANCELED } + .mapLatest { + it.transitionState != TransitionState.FINISHED && + fromStatePredicate(it.from) && + toStatePredicate(it.to) } .distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index bf763b4e1f99..400b8bfff9b0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -30,7 +30,10 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators +import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -48,6 +51,7 @@ object KeyguardClockViewBinder { keyguardRootView: ConstraintLayout, viewModel: KeyguardClockViewModel, keyguardClockInteractor: KeyguardClockInteractor, + blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -61,18 +65,16 @@ object KeyguardClockViewBinder { viewModel.currentClock.collect { currentClock -> cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) viewModel.clock = currentClock - addClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) - viewModel.burnInLayer?.updatePostLayout(keyguardRootView) - applyConstraints(clockSection, keyguardRootView, true) + addClockViews(currentClock, keyguardRootView) + updateBurnInLayer(keyguardRootView, viewModel) + blueprintInteractor.refreshBlueprint() } } - // TODO: Weather clock dozing animation - // will trigger both shouldBeCentered and clockSize change - // we should avoid this launch { if (!migrateClocksToBlueprint()) return@launch viewModel.clockSize.collect { - applyConstraints(clockSection, keyguardRootView, true) + updateBurnInLayer(keyguardRootView, viewModel) + blueprintInteractor.refreshBlueprint() } } launch { @@ -82,7 +84,7 @@ object KeyguardClockViewBinder { if (it.largeClock.config.hasCustomPositionUpdatedAnimation) { playClockCenteringAnimation(clockSection, keyguardRootView, it) } else { - applyConstraints(clockSection, keyguardRootView, true) + blueprintInteractor.refreshBlueprint() } } } @@ -90,6 +92,29 @@ object KeyguardClockViewBinder { } } } + @VisibleForTesting + fun updateBurnInLayer( + keyguardRootView: ConstraintLayout, + viewModel: KeyguardClockViewModel, + ) { + val burnInLayer = viewModel.burnInLayer + val clockController = viewModel.currentClock.value + clockController?.let { clock -> + when (viewModel.clockSize.value) { + LARGE -> { + clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) } + if (clock.config.useAlternateSmartspaceAODTransition) { + clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) } + } + } + SMALL -> { + clock.smallClock.layout.views.forEach { burnInLayer?.addView(it) } + clock.largeClock.layout.views.forEach { burnInLayer?.removeView(it) } + } + } + } + viewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } private fun cleanupClockViews( clockController: ClockController?, @@ -116,7 +141,6 @@ object KeyguardClockViewBinder { fun addClockViews( clockController: ClockController?, rootView: ConstraintLayout, - burnInLayer: Layer? ) { clockController?.let { clock -> clock.smallClock.layout.views[0].id = R.id.lockscreen_clock_view @@ -125,17 +149,10 @@ object KeyguardClockViewBinder { } // small clock should either be a single view or container with id // `lockscreen_clock_view` - clock.smallClock.layout.views.forEach { - rootView.addView(it) - burnInLayer?.addView(it) - } + clock.smallClock.layout.views.forEach { rootView.addView(it) } clock.largeClock.layout.views.forEach { rootView.addView(it) } - if (clock.config.useAlternateSmartspaceAODTransition) { - clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) } - } } } - fun applyConstraints( clockSection: ClockSection, rootView: ConstraintLayout, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index 81ce8f04d302..10392e3c1450 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -16,15 +16,13 @@ package com.android.systemui.keyguard.ui.binder -import android.transition.TransitionManager import android.view.View import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -35,10 +33,10 @@ import kotlinx.coroutines.launch object KeyguardSmartspaceViewBinder { @JvmStatic fun bind( - smartspaceSection: SmartspaceSection, keyguardRootView: ConstraintLayout, clockViewModel: KeyguardClockViewModel, smartspaceViewModel: KeyguardSmartspaceViewModel, + blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -46,22 +44,56 @@ object KeyguardSmartspaceViewBinder { if (!migrateClocksToBlueprint()) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay -> - if (hasCustomWeatherDataDisplay) { - removeDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) - } else { - addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) - } - clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) - val constraintSet = ConstraintSet().apply { clone(keyguardRootView) } - smartspaceSection.applyConstraints(constraintSet) - TransitionManager.beginDelayedTransition(keyguardRootView) - constraintSet.applyTo(keyguardRootView) + updateDateWeatherToBurnInLayer( + keyguardRootView, + clockViewModel, + smartspaceViewModel + ) + blueprintInteractor.refreshBlueprint() + } + } + + launch { + smartspaceViewModel.bcSmartspaceVisibility.collect { + updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) + blueprintInteractor.refreshBlueprint() } } } } } + private fun updateBCSmartspaceInBurnInLayer( + keyguardRootView: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + ) { + // Visibility is controlled by updateTargetVisibility in CardPagerAdapter + val burnInLayer = keyguardRootView.requireViewById<Layer>(R.id.burn_in_layer) + burnInLayer.apply { + val smartspaceView = + keyguardRootView.requireViewById<View>(sharedR.id.bc_smartspace_view) + if (smartspaceView.visibility == View.VISIBLE) { + addView(smartspaceView) + } else { + removeView(smartspaceView) + } + } + clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } + + private fun updateDateWeatherToBurnInLayer( + keyguardRootView: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + smartspaceViewModel: KeyguardSmartspaceViewModel + ) { + if (clockViewModel.hasCustomWeatherDataDisplay.value) { + removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel) + } else { + addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) + } + clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } + private fun addDateWeatherToBurnInLayer( constraintLayout: ConstraintLayout, smartspaceViewModel: KeyguardSmartspaceViewModel @@ -76,13 +108,13 @@ object KeyguardSmartspaceViewBinder { constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view) val weatherView = constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view) - addView(weatherView) addView(dateView) + addView(weatherView) } } } - private fun removeDateWeatherToBurnInLayer( + private fun removeDateWeatherFromBurnInLayer( constraintLayout: ConstraintLayout, smartspaceViewModel: KeyguardSmartspaceViewModel ) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 24d06026dcf7..8472a9f6da6d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -23,6 +23,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection +import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection @@ -30,11 +31,10 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection +import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeSmartspaceSection import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject @@ -62,8 +62,8 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, communalTutorialIndicatorSection: CommunalTutorialIndicatorSection, - smartspaceSection: SplitShadeSmartspaceSection, - clockSection: SplitShadeClockSection, + clockSection: ClockSection, + smartspaceSection: SmartspaceSection, mediaSection: SplitShadeMediaSection, ) : KeyguardBlueprint { override val id: String = ID diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index d0626d58a4ad..fd530b77707a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -25,6 +25,8 @@ import android.transition.Visibility import android.view.View import android.view.ViewGroup import androidx.constraintlayout.helper.widget.Layer +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.res.R class BaseBlueprintTransition : TransitionSet() { init { @@ -33,7 +35,16 @@ class BaseBlueprintTransition : TransitionSet() { .addTransition(ChangeBounds()) .addTransition(AlphaInVisibility()) excludeTarget(Layer::class.java, /* exclude= */ true) + excludeClockAndSmartspaceViews() } + + private fun excludeClockAndSmartspaceViews() { + excludeTarget(R.id.lockscreen_clock_view, true) + excludeTarget(R.id.lockscreen_clock_view_large, true) + excludeTarget(SmartspaceView::class.java, true) + // TODO(b/319468190): need to exclude views from large weather clock + } + class AlphaOutVisibility : Visibility() { override fun onDisappear( sceneRoot: ViewGroup?, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt new file mode 100644 index 000000000000..67a20e588198 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt @@ -0,0 +1,73 @@ +/* + * 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.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.View +import androidx.constraintlayout.helper.widget.Layer + +class AodBurnInLayer(context: Context) : Layer(context) { + // For setScale in Layer class, it stores it in mScaleX/Y and directly apply scale to + // referenceViews instead of keeping the value in fields of View class + // when we try to clone ConstraintSet, it will call getScaleX from View class and return 1.0 + // and when we clone and apply, it will reset everything in the layer + // which cause the flicker from AOD to LS + private var _scaleX = 1F + private var _scaleY = 1F + // As described for _scaleX and _scaleY, we have similar issue with translation + private var _translationX = 1F + private var _translationY = 1F + // avoid adding views with same ids + override fun addView(view: View?) { + view?.let { if (it.id !in referencedIds) super.addView(view) } + } + override fun setScaleX(scaleX: Float) { + _scaleX = scaleX + super.setScaleX(scaleX) + } + + override fun getScaleX(): Float { + return _scaleX + } + + override fun setScaleY(scaleY: Float) { + _scaleY = scaleY + super.setScaleY(scaleY) + } + + override fun getScaleY(): Float { + return _scaleY + } + + override fun setTranslationX(dx: Float) { + _translationX = dx + super.setTranslationX(dx) + } + + override fun getTranslationX(): Float { + return _translationX + } + + override fun setTranslationY(dy: Float) { + _translationY = dy + super.setTranslationY(dy) + } + + override fun getTranslationY(): Float { + return _translationY + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 1ccc6ccf2cec..3d36eb03a1bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -19,16 +19,13 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View -import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import javax.inject.Inject /** Adds a layer to group elements for translation for burn-in preventation */ @@ -37,10 +34,8 @@ class AodBurnInSection constructor( private val context: Context, private val clockViewModel: KeyguardClockViewModel, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, ) : KeyguardSection() { - lateinit var burnInLayer: Layer - + private lateinit var burnInLayer: AodBurnInLayer override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { return @@ -48,7 +43,7 @@ constructor( val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container) burnInLayer = - Layer(context).apply { + AodBurnInLayer(context).apply { id = R.id.burn_in_layer addView(nic) if (!migrateClocksToBlueprint()) { @@ -57,11 +52,6 @@ constructor( addView(statusView) } } - if (migrateClocksToBlueprint()) { - // weather and date parts won't be added here, cause their visibility doesn't align - // with others in burnInLayer - addSmartspaceViews(constraintLayout) - } constraintLayout.addView(burnInLayer) } @@ -83,14 +73,4 @@ constructor( override fun removeViews(constraintLayout: ConstraintLayout) { constraintLayout.removeView(R.id.burn_in_layer) } - - private fun addSmartspaceViews(constraintLayout: ConstraintLayout) { - burnInLayer.apply { - if (smartspaceViewModel.isSmartspaceEnabled) { - val smartspaceView = - constraintLayout.requireViewById<View>(sharedR.id.bc_smartspace_view) - addView(smartspaceView) - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index f560b5f068c9..ed7abff555e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -30,9 +30,7 @@ import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker @@ -53,13 +51,13 @@ constructor( private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val notificationIconAreaController: NotificationIconAreaController, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val systemBarUtilsState: SystemBarUtilsState, ) : KeyguardSection() { private var nicBindingDisposable: DisposableHandle? = null private val nicId = R.id.aod_notification_icon_container private lateinit var nic: NotificationIconContainer + private val smartSpaceBarrier = View.generateViewId() override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { @@ -118,7 +116,7 @@ constructor( } constraintSet.apply { if (migrateClocksToBlueprint()) { - connect(nicId, TOP, sharedR.id.bc_smartspace_view, BOTTOM, bottomMargin) + connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin) setGoneMargin(nicId, BOTTOM, bottomMargin) } else { connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index b5f32c8a5608..b344d3b9afea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -23,11 +23,14 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder @@ -35,8 +38,10 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.res.R +import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils +import dagger.Lazy import javax.inject.Inject internal fun ConstraintSet.setVisibility( @@ -56,6 +61,7 @@ constructor( protected val keyguardClockViewModel: KeyguardClockViewModel, private val context: Context, private val splitShadeStateController: SplitShadeStateController, + val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) {} @@ -68,6 +74,7 @@ constructor( constraintLayout, keyguardClockViewModel, clockInteractor, + blueprintInteractor.get() ) } @@ -88,12 +95,16 @@ constructor( ): ConstraintSet { // Add constraint between rootView and clockContainer applyDefaultConstraints(constraintSet) + getNonTargetClockFace(clock).applyConstraints(constraintSet) getTargetClockFace(clock).applyConstraints(constraintSet) // Add constraint between elements in clock and clock container return constraintSet.apply { - setAlpha(getTargetClockFace(clock).views, 1F) - setAlpha(getNonTargetClockFace(clock).views, 0F) + setVisibility(getTargetClockFace(clock).views, VISIBLE) + setVisibility(getNonTargetClockFace(clock).views, INVISIBLE) + if (!keyguardClockViewModel.useLargeClock) { + connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM) + } } } @@ -107,9 +118,12 @@ constructor( private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout open fun applyDefaultConstraints(constraints: ConstraintSet) { + val guideline = + if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID + else R.id.split_shade_guideline constraints.apply { connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START) - connect(R.id.lockscreen_clock_view_large, END, PARENT_ID, END) + connect(R.id.lockscreen_clock_view_large, END, guideline, END) connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP) var largeClockTopMargin = context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index b0eee0a68b1f..8c5e9b4c6817 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.view.View import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END @@ -27,11 +28,9 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator @@ -54,7 +53,6 @@ constructor( ambientState: AmbientState, controller: NotificationStackScrollLayoutController, notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( @@ -69,6 +67,7 @@ constructor( notificationStackSizeCalculator, mainDispatcher, ) { + private val smartSpaceBarrier = View.generateViewId() override fun applyConstraints(constraintSet: ConstraintSet) { if (!KeyguardShadeMigrationNssl.isEnabled) { return @@ -76,16 +75,14 @@ constructor( constraintSet.apply { val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - if (migrateClocksToBlueprint()) { connect( R.id.nssl_placeholder, TOP, - sharedR.id.bc_smartspace_view, + R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin ) - setGoneMargin(R.id.nssl_placeholder, TOP, bottomMargin) } else { connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 2a68f26d3ae7..0c0eb8a673a4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT +import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -96,6 +97,11 @@ constructor( constrainHeight(R.id.end_button, height) connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT, horizontalOffsetMargin) connect(R.id.end_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin) + + // The constraint set visibility for start and end button are default visible, set to + // ignore so the view's own initial visibility (invisible) is used + setVisibilityMode(R.id.start_button, VISIBILITY_MODE_IGNORE) + setVisibilityMode(R.id.end_button, VISIBILITY_MODE_IGNORE) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index eacd466bc473..37842a84c3d3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -18,37 +18,41 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View +import android.view.View.GONE +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.END -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.START -import androidx.constraintlayout.widget.ConstraintSet.TOP -import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.res.R +import com.android.systemui.shared.R import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import dagger.Lazy import javax.inject.Inject open class SmartspaceSection @Inject constructor( + val context: Context, val keyguardClockViewModel: KeyguardClockViewModel, val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, - private val context: Context, + val keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor, val smartspaceController: LockscreenSmartspaceController, val keyguardUnlockAnimationController: KeyguardUnlockAnimationController, + val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { private var smartspaceView: View? = null private var weatherView: View? = null private var dateView: View? = null + private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null + override fun addViews(constraintLayout: ConstraintLayout) { if (!migrateClocksToBlueprint()) { return @@ -64,6 +68,20 @@ constructor( } } keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView + smartspaceVisibilityListener = + object : OnGlobalLayoutListener { + var pastVisibility = GONE + override fun onGlobalLayout() { + smartspaceView?.let { + val newVisibility = it.visibility + if (pastVisibility != newVisibility) { + keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility) + pastVisibility = newVisibility + } + } + } + } + smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener) } override fun bindData(constraintLayout: ConstraintLayout) { @@ -71,10 +89,10 @@ constructor( return } KeyguardSmartspaceViewBinder.bind( - this, constraintLayout, keyguardClockViewModel, keyguardSmartspaceViewModel, + blueprintInteractor.get(), ) } @@ -82,65 +100,96 @@ constructor( if (!migrateClocksToBlueprint()) { return } - // Generally, weather should be next to dateView - // smartspace should be below date & weather views constraintSet.apply { // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController - dateView?.let { dateView -> - constrainHeight(dateView.id, WRAP_CONTENT) - constrainWidth(dateView.id, WRAP_CONTENT) - connect( - dateView.id, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.date_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_start ) - } - weatherView?.let { - constrainWidth(it.id, WRAP_CONTENT) - dateView?.let { dateView -> - connect(it.id, TOP, dateView.id, TOP) - connect(it.id, BOTTOM, dateView.id, BOTTOM) - connect(it.id, START, dateView.id, END, 4) - } - } + ) + constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.weather_smartspace_view, + ConstraintSet.TOP, + R.id.date_smartspace_view, + ConstraintSet.TOP + ) + connect( + R.id.weather_smartspace_view, + ConstraintSet.BOTTOM, + R.id.date_smartspace_view, + ConstraintSet.BOTTOM + ) + connect( + R.id.weather_smartspace_view, + ConstraintSet.START, + R.id.date_smartspace_view, + ConstraintSet.END, + 4 + ) + // migrate addSmartspaceView from KeyguardClockSwitchController - smartspaceView?.let { - constrainHeight(it.id, WRAP_CONTENT) - connect( - it.id, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.bc_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_start ) - connect( - it.id, - END, - PARENT_ID, - END, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) + ) + connect( + R.id.bc_smartspace_view, + ConstraintSet.END, + if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID + else com.android.systemui.res.R.id.split_shade_guideline, + ConstraintSet.END, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_end ) - } + ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - dateView?.let { dateView -> - smartspaceView?.let { smartspaceView -> - connect(dateView.id, BOTTOM, smartspaceView.id, TOP) - } - } + clear(R.id.date_smartspace_view, ConstraintSet.TOP) + connect( + R.id.date_smartspace_view, + ConstraintSet.BOTTOM, + R.id.bc_smartspace_view, + ConstraintSet.TOP + ) } else { - dateView?.let { dateView -> - clear(dateView.id, BOTTOM) - connect(dateView.id, TOP, R.id.lockscreen_clock_view, BOTTOM) - constrainHeight(dateView.id, WRAP_CONTENT) - smartspaceView?.let { smartspaceView -> - clear(smartspaceView.id, TOP) - connect(smartspaceView.id, TOP, dateView.id, BOTTOM) - } - } + clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM) + connect( + R.id.date_smartspace_view, + ConstraintSet.TOP, + com.android.systemui.res.R.id.lockscreen_clock_view, + ConstraintSet.BOTTOM + ) + connect( + R.id.bc_smartspace_view, + ConstraintSet.TOP, + R.id.date_smartspace_view, + ConstraintSet.BOTTOM + ) } + + createBarrier( + com.android.systemui.res.R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.bc_smartspace_view, + R.id.date_smartspace_view, + R.id.weather_smartspace_view, + ) + ) } updateVisibility(constraintSet) } @@ -156,30 +205,28 @@ constructor( } } } + smartspaceView?.viewTreeObserver?.removeOnGlobalLayoutListener(smartspaceVisibilityListener) + smartspaceVisibilityListener = null } private fun updateVisibility(constraintSet: ConstraintSet) { constraintSet.apply { - weatherView?.let { - setVisibility( - it.id, - when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - true -> ConstraintSet.GONE - false -> - when (keyguardSmartspaceViewModel.isWeatherEnabled) { - true -> ConstraintSet.VISIBLE - false -> ConstraintSet.GONE - } - } - ) - } - dateView?.let { - setVisibility( - it.id, - if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE - else ConstraintSet.VISIBLE - ) - } + setVisibility( + R.id.weather_smartspace_view, + when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { + true -> ConstraintSet.GONE + false -> + when (keyguardSmartspaceViewModel.isWeatherEnabled) { + true -> ConstraintSet.VISIBLE + false -> ConstraintSet.GONE + } + } + ) + setVisibility( + R.id.date_smartspace_view, + if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE + else ConstraintSet.VISIBLE + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt deleted file mode 100644 index 19ba1aa4763a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.res.R -import com.android.systemui.statusbar.policy.SplitShadeStateController -import javax.inject.Inject - -class SplitShadeClockSection -@Inject -constructor( - clockInteractor: KeyguardClockInteractor, - keyguardClockViewModel: KeyguardClockViewModel, - context: Context, - splitShadeStateController: SplitShadeStateController, -) : ClockSection(clockInteractor, keyguardClockViewModel, context, splitShadeStateController) { - override fun applyDefaultConstraints(constraints: ConstraintSet) { - super.applyDefaultConstraints(constraints) - val largeClockEndGuideline = - if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID - else R.id.split_shade_guideline - constraints.apply { - connect( - R.id.lockscreen_clock_view_large, - ConstraintSet.END, - largeClockEndGuideline, - ConstraintSet.END - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index f20ab06bcda9..b12a8a811955 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -20,7 +20,6 @@ import android.content.Context import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout -import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -31,11 +30,9 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.media.controls.ui.KeyguardMediaController import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.shared.R as sharedR import javax.inject.Inject /** Aligns media on left side for split shade, below smartspace, date, and weather. */ @@ -44,11 +41,9 @@ class SplitShadeMediaSection constructor( private val context: Context, private val notificationPanelView: NotificationPanelView, - private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, private val keyguardMediaController: KeyguardMediaController ) : KeyguardSection() { private val mediaContainerId = R.id.status_view_media_container - private val smartSpaceBarrier = R.id.smart_space_barrier_bottom override fun addViews(constraintLayout: ConstraintLayout) { if (!migrateClocksToBlueprint()) { @@ -85,18 +80,7 @@ constructor( constraintSet.apply { constrainWidth(mediaContainerId, MATCH_CONSTRAINT) constrainHeight(mediaContainerId, WRAP_CONTENT) - - createBarrier( - smartSpaceBarrier, - Barrier.BOTTOM, - 0, - *intArrayOf( - sharedR.id.bc_smartspace_view, - sharedR.id.date_smartspace_view, - sharedR.id.weather_smartspace_view, - ) - ) - connect(mediaContainerId, TOP, smartSpaceBarrier, BOTTOM) + connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) connect(mediaContainerId, START, PARENT_ID, START) connect(mediaContainerId, END, R.id.split_shade_guideline, END) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt deleted file mode 100644 index 8728adadd8c3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import com.android.systemui.keyguard.KeyguardUnlockAnimationController -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import javax.inject.Inject - -/* - * We need this class for the splitShadeBlueprint so `addViews` and `removeViews` will be called - * when switching to and from splitShade. - */ -class SplitShadeSmartspaceSection -@Inject -constructor( - keyguardClockViewModel: KeyguardClockViewModel, - keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, - context: Context, - smartspaceController: LockscreenSmartspaceController, - keyguardUnlockAnimationController: KeyguardUnlockAnimationController, -) : - SmartspaceSection( - keyguardClockViewModel, - keyguardSmartspaceViewModel, - context, - smartspaceController, - keyguardUnlockAnimationController, - ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index a1dd720a82f1..e8c1ab5feccc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -33,6 +34,7 @@ constructor( @Application applicationScope: CoroutineScope, smartspaceController: LockscreenSmartspaceController, keyguardClockViewModel: KeyguardClockViewModel, + smartspaceInteractor: KeyguardSmartspaceInteractor, ) { /** Whether the smartspace section is available in the build. */ val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled() @@ -78,4 +80,7 @@ constructor( ): Boolean { return !clockIncludesCustomWeatherDisplay && isWeatherEnabled } + + /* trigger clock and smartspace constraints change when smartspace appears */ + var bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt index 252945f1ed6a..5910701d9f2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package com.android.systemui.statusbar.pipeline.dagger +package com.android.systemui.log.dagger -import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository import javax.inject.Qualifier -/** Detailed [DeviceBasedSatelliteRepository] logs */ +/** A [com.android.systemui.log.LogBuffer] for keyboard-related functionality. */ @Qualifier @MustBeDocumented -@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) -annotation class OemSatelliteInputLog +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyboardLog 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 24cb8fff9b67..3e0094081638 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -603,6 +603,14 @@ public class LogModule { return factory.create("BluetoothTileDialogLog", 50); } + /** Provides a {@link LogBuffer} for the keyboard functionalities. */ + @Provides + @SysUISingleton + @KeyboardLog + public static LogBuffer provideKeyboardLogBuffer(LogBufferFactory factory) { + return factory.create("KeyboardLog", 50); + } + /** Provides a {@link LogBuffer} for {@link PackageChangeRepository} */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 0d641ac9c688..58e042868607 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -624,7 +624,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } if (!mIsEnabled) { - mGestureNavigationSettingsObserver.unregister(); + mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::unregister); if (DEBUG_MISSING_GESTURE) { Log.d(DEBUG_MISSING_GESTURE_TAG, "Unregister display listener"); } @@ -642,7 +642,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } } else { - mGestureNavigationSettingsObserver.register(); + mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::register); updateDisplaySize(); if (DEBUG_MISSING_GESTURE) { Log.d(DEBUG_MISSING_GESTURE_TAG, "Register display listener"); diff --git a/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt b/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt new file mode 100644 index 000000000000..ca790e830f7f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt @@ -0,0 +1,75 @@ +/* + * 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.qs + +import android.view.KeyEvent +import android.view.View +import androidx.core.util.Consumer + +/** + * Listens for left and right arrow keys pressed while focus is on the view. + * + * Key press is treated as correct when its full lifecycle happened on the view: first + * [KeyEvent.ACTION_DOWN] was performed, view didn't lose focus in the meantime and then + * [KeyEvent.ACTION_UP] was performed with the same [KeyEvent.getKeyCode] + */ +class LeftRightArrowPressedListener private constructor() : + View.OnKeyListener, View.OnFocusChangeListener { + + private var lastKeyCode: Int? = 0 + private var listener: Consumer<Int>? = null + + fun setArrowKeyPressedListener(arrowPressedListener: Consumer<Int>) { + listener = arrowPressedListener + } + + override fun onKey(view: View, keyCode: Int, keyEvent: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need + // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we + // have a chance to intercept ACTION_UP. + if (keyEvent.action == KeyEvent.ACTION_UP && keyCode == lastKeyCode) { + listener?.accept(keyCode) + lastKeyCode = null + } else if (keyEvent.repeatCount == 0) { + // we only read key events that are NOT coming from long pressing because that also + // causes reading ACTION_DOWN event (with repeated count > 0) when moving focus with + // arrow from another sibling view + lastKeyCode = keyCode + } + return true + } + return false + } + + override fun onFocusChange(view: View, hasFocus: Boolean) { + // resetting lastKeyCode so we get fresh cleared state on focus + if (hasFocus) { + lastKeyCode = null + } + } + + companion object { + @JvmStatic + fun createAndRegisterListenerForView(view: View): LeftRightArrowPressedListener { + val listener = LeftRightArrowPressedListener() + view.setOnKeyListener(listener) + view.onFocusChangeListener = listener + return listener + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java index 4770d5272508..6fb5174cc612 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java @@ -1,7 +1,11 @@ package com.android.systemui.qs; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT; + import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Animatable2; @@ -9,10 +13,12 @@ import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; +import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import com.android.settingslib.Utils; @@ -36,13 +42,14 @@ public class PageIndicator extends ViewGroup { private final ArrayList<Integer> mQueuedPositions = new ArrayList<>(); - private final int mPageIndicatorWidth; - private final int mPageIndicatorHeight; - private final int mPageDotWidth; + private int mPageIndicatorWidth; + private int mPageIndicatorHeight; + private int mPageDotWidth; private @NonNull ColorStateList mTint; private int mPosition = -1; private boolean mAnimating; + private PageScrollActionListener mPageScrollActionListener; private final Animatable2.AnimationCallback mAnimationCallback = new Animatable2.AnimationCallback() { @@ -77,6 +84,43 @@ public class PageIndicator extends ViewGroup { mPageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width); mPageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height); mPageDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width); + LeftRightArrowPressedListener arrowListener = + LeftRightArrowPressedListener.createAndRegisterListenerForView(this); + arrowListener.setArrowKeyPressedListener(keyCode -> { + if (mPageScrollActionListener != null) { + int swipeDirection = keyCode == KeyEvent.KEYCODE_DPAD_LEFT ? LEFT : RIGHT; + mPageScrollActionListener.onScrollActionTriggered(swipeDirection); + } + }); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateResources(); + } + + private void updateResources() { + Resources res = getResources(); + boolean changed = false; + int pageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width); + if (pageIndicatorWidth != mPageIndicatorWidth) { + mPageIndicatorWidth = pageIndicatorWidth; + changed = true; + } + int pageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height); + if (pageIndicatorHeight != mPageIndicatorHeight) { + mPageIndicatorHeight = pageIndicatorHeight; + changed = true; + } + int pageIndicatorDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width); + if (pageIndicatorDotWidth != mPageDotWidth) { + mPageDotWidth = pageIndicatorDotWidth; + changed = true; + } + if (changed) { + invalidate(); + } } public void setNumPages(int numPages) { @@ -280,4 +324,19 @@ public class PageIndicator extends ViewGroup { getChildAt(i).layout(left, 0, mPageIndicatorWidth + left, mPageIndicatorHeight); } } + + void setPageScrollActionListener(PageScrollActionListener listener) { + mPageScrollActionListener = listener; + } + + interface PageScrollActionListener { + + @IntDef({LEFT, RIGHT}) + @interface Direction { } + + int LEFT = 0; + int RIGHT = 1; + + void onScrollActionTriggered(@Direction int swipeDirection); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 052c0daf0b56..43f3a2242da4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -1,6 +1,8 @@ package com.android.systemui.qs; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -12,7 +14,6 @@ import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; import android.util.AttributeSet; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,6 +31,7 @@ import androidx.viewpager.widget.ViewPager; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.PageIndicator.PageScrollActionListener.Direction; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanelControllerBase.TileRecord; import com.android.systemui.qs.logging.QSLogger; @@ -310,26 +312,18 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mPageIndicator = indicator; mPageIndicator.setNumPages(mPages.size()); mPageIndicator.setLocation(mPageIndicatorPosition); - mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> { - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need - // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we - // have a chance to intercept ACTION_UP. - if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) { - scrollByX(getDeltaXForKeyboardScrolling(keyCode), - SINGLE_PAGE_SCROLL_DURATION_MILLIS); - } - return true; + mPageIndicator.setPageScrollActionListener(swipeDirection -> { + if (mScroller.isFinished()) { + scrollByX(getDeltaXForPageScrolling(swipeDirection), + SINGLE_PAGE_SCROLL_DURATION_MILLIS); } - return false; }); } - private int getDeltaXForKeyboardScrolling(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) { + private int getDeltaXForPageScrolling(@Direction int swipeDirection) { + if (swipeDirection == LEFT && getCurrentItem() != 0) { return -getWidth(); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - && getCurrentItem() != mPages.size() - 1) { + } else if (swipeDirection == RIGHT && getCurrentItem() != mPages.size() - 1) { return getWidth(); } return 0; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java index c908e6e2afbe..5a872d699f35 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java @@ -35,6 +35,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settingslib.development.DevelopmentSettingsEnabler; +import com.android.systemui.FontSizeUtils; import com.android.systemui.res.R; /** @@ -109,11 +110,31 @@ public class QSFooterView extends FrameLayout { private void updateResources() { updateFooterAnimator(); + updateEditButtonResources(); + updateBuildTextResources(); MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); + lp.height = getResources().getDimensionPixelSize(R.dimen.qs_footer_height); + int sideMargin = getResources().getDimensionPixelSize(R.dimen.qs_footer_margin); + lp.leftMargin = sideMargin; + lp.rightMargin = sideMargin; lp.bottomMargin = getResources().getDimensionPixelSize(R.dimen.qs_footers_margin_bottom); setLayoutParams(lp); } + private void updateEditButtonResources() { + int size = getResources().getDimensionPixelSize(R.dimen.qs_footer_action_button_size); + int padding = getResources().getDimensionPixelSize(R.dimen.qs_footer_icon_padding); + MarginLayoutParams lp = (MarginLayoutParams) mEditButton.getLayoutParams(); + lp.height = size; + lp.width = size; + mEditButton.setLayoutParams(lp); + mEditButton.setPadding(padding, padding, padding, padding); + } + + private void updateBuildTextResources() { + FontSizeUtils.updateFontSizeFromStyle(mBuildText, R.style.TextAppearance_QS_Status_Build); + } + private void updateFooterAnimator() { mFooterAnimator = createFooterAnimator(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index aff4a6759a47..2077d733172d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -88,9 +88,9 @@ class FooterActionsViewModel( /** Called when the expansion of the Quick Settings changed. */ fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) { if (isInSplitShade) { - // In split shade, we want to fade in the background only at the very end (see - // b/240563302). - val delay = 0.99f + // In split shade, we want to fade in the background when the QS background starts to + // show. + val delay = 0.15f _alpha.value = expansion _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay) } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt index 1805eb182cb1..1a06c38803af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt @@ -22,12 +22,14 @@ import android.view.LayoutInflater import android.view.View import android.view.View.AccessibilityDelegate import android.view.View.GONE +import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageView +import android.widget.ProgressBar import android.widget.Switch import android.widget.TextView import androidx.recyclerview.widget.AsyncListDiffer @@ -92,6 +94,8 @@ constructor( private lateinit var pairNewDeviceButton: View private lateinit var deviceListView: RecyclerView private lateinit var scrollViewContent: View + private lateinit var progressBarAnimation: ProgressBar + private lateinit var progressBarBackground: View override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -122,6 +126,8 @@ constructor( scrollViewContent = this layoutParams.height = cachedContentHeight } + progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation) + progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background) } override fun start() { @@ -135,6 +141,17 @@ constructor( super.dismiss() } + internal suspend fun animateProgressBar(animate: Boolean) { + withContext(mainDispatcher) { + if (animate) { + showProgressBar() + } else { + delay(PROGRESS_BAR_ANIMATION_DURATION_MS) + hideProgressBar() + } + } + } + internal suspend fun onDeviceItemUpdated( deviceItem: List<DeviceItem>, showSeeAll: Boolean, @@ -190,6 +207,28 @@ constructor( } } + private fun showProgressBar() { + if ( + ::progressBarAnimation.isInitialized && + ::progressBarBackground.isInitialized && + progressBarAnimation.visibility != VISIBLE + ) { + progressBarAnimation.visibility = VISIBLE + progressBarBackground.visibility = INVISIBLE + } + } + + private fun hideProgressBar() { + if ( + ::progressBarAnimation.isInitialized && + ::progressBarBackground.isInitialized && + progressBarAnimation.visibility != INVISIBLE + ) { + progressBarAnimation.visibility = INVISIBLE + progressBarBackground.visibility = VISIBLE + } + } + internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { @@ -300,6 +339,7 @@ constructor( const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS" const val DISABLED_ALPHA = 0.3f const val ENABLED_ALPHA = 1f + const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L private fun Boolean.toInt(): Int { return if (this) 1 else 0 diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt index 6d08f591690f..194e7bc955c9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt @@ -105,22 +105,29 @@ constructor( deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD) } - bluetoothStateInteractor.bluetoothStateUpdate - .filterNotNull() + // deviceItemUpdate is emitted when device item list is done fetching, update UI and + // stop the progress bar. + deviceItemInteractor.deviceItemUpdate .onEach { - dialog.onBluetoothStateUpdated(it, getSubtitleResId(it)) - updateDeviceItemJob?.cancel() - updateDeviceItemJob = launch { - deviceItemInteractor.updateDeviceItems( - context, - DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED - ) + updateDialogUiJob?.cancel() + updateDialogUiJob = launch { + dialog.apply { + onDeviceItemUpdated( + it.take(MAX_DEVICE_ITEM_ENTRY), + showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY, + showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled + ) + animateProgressBar(false) + } } } .launchIn(this) + // deviceItemUpdateRequest is emitted when a bluetooth callback is called, re-fetch + // the device item list and animiate the progress bar. deviceItemInteractor.deviceItemUpdateRequest .onEach { + dialog.animateProgressBar(true) updateDeviceItemJob?.cancel() updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems( @@ -131,27 +138,37 @@ constructor( } .launchIn(this) - deviceItemInteractor.deviceItemUpdate + // bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch + // the device item list. + bluetoothStateInteractor.bluetoothStateUpdate + .filterNotNull() .onEach { - updateDialogUiJob?.cancel() - updateDialogUiJob = launch { - dialog.onDeviceItemUpdated( - it.take(MAX_DEVICE_ITEM_ENTRY), - showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY, - showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled + dialog.onBluetoothStateUpdated(it, getSubtitleResId(it)) + updateDeviceItemJob?.cancel() + updateDeviceItemJob = launch { + deviceItemInteractor.updateDeviceItems( + context, + DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED ) } } .launchIn(this) + // bluetoothStateToggle is emitted when user toggles the bluetooth state switch, + // send the new value to the bluetoothStateInteractor and animate the progress bar. dialog.bluetoothStateToggle - .onEach { bluetoothStateInteractor.isBluetoothEnabled = it } + .onEach { + dialog.animateProgressBar(true) + bluetoothStateInteractor.isBluetoothEnabled = it + } .launchIn(this) + // deviceItemClick is emitted when user clicked on a device item. dialog.deviceItemClick .onEach { deviceItemInteractor.updateDeviceItemOnClick(it) } .launchIn(this) + // contentHeight is emitted when the dialog is dismissed. dialog.contentHeight .onEach { withContext(backgroundDispatcher) { diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt index a6cccf1cb41b..e2959fe834d4 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt @@ -24,7 +24,9 @@ import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository import com.android.systemui.statusbar.NotificationPresenter +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.init.NotificationsController +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.policy.HeadsUpManager import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -44,6 +46,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val headsUpManager: HeadsUpManager, private val powerInteractor: PowerInteractor, + private val activeNotificationsInteractor: ActiveNotificationsInteractor, ) : CoreStartable { private var notificationPresenter: NotificationPresenter? = null @@ -117,6 +120,14 @@ constructor( return if (headsUpManager.hasPinnedHeadsUp() && isNotifPresenterFullyCollapsed) { 1 } else { + getActiveNotificationsCount() + } + } + + private fun getActiveNotificationsCount(): Int { + return if (NotificationsLiveDataStoreRefactor.isEnabled) { + activeNotificationsInteractor.allNotificationsCountValue + } else { notificationsController?.getActiveNotificationsCount() ?: 0 } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index ab08f660789c..a755805d1872 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -37,6 +37,7 @@ object SceneContainerFlag { /** The flag description -- not an aconfig flag name */ const val DESCRIPTION = "SceneContainerFlag" + @JvmStatic inline val isEnabled get() = SCENE_CONTAINER_ENABLED && // mainStaticFlag diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 4a839b8df9ba..93cfc5dbcbe3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -85,12 +85,13 @@ object SceneWindowRootViewBinder { view.addView( ComposeFacade.createSceneContainerView( - scope = this, - context = view.context, - viewModel = viewModel, - windowInsets = windowInsets, - sceneByKey = sortedSceneByKey, - ) + scope = this, + context = view.context, + viewModel = viewModel, + windowInsets = windowInsets, + sceneByKey = sortedSceneByKey, + ) + .also { it.id = R.id.scene_container_root_composable } ) val legacyView = view.requireViewById<View>(R.id.legacy_window_root) diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt new file mode 100644 index 000000000000..b09bfe21e014 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt @@ -0,0 +1,43 @@ +/* + * 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.settings + +import android.annotation.UserIdInt +import android.content.Context +import android.content.SharedPreferences +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Extension functions for [UserFileManager]. */ +object UserFileManagerExt { + + /** Returns a flow of [Unit] that is invoked each time the shared preference is updated. */ + fun UserFileManager.observeSharedPreferences( + fileName: String, + @Context.PreferencesMode mode: Int, + @UserIdInt userId: Int + ): Flow<Unit> = conflatedCallbackFlow { + val sharedPrefs = getSharedPreferences(fileName, mode, userId) + + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> trySend(Unit) } + + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index d0da945ca7d0..9af2d58910b6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -20,15 +20,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaHierarchyManager -import com.android.systemui.media.controls.ui.MediaHost -import com.android.systemui.media.controls.ui.MediaHostState -import com.android.systemui.media.dagger.MediaModule import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject -import javax.inject.Named import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -46,7 +41,6 @@ constructor( val shadeHeaderViewModel: ShadeHeaderViewModel, val notifications: NotificationsPlaceholderViewModel, val mediaDataManager: MediaDataManager, - @Named(MediaModule.QUICK_QS_PANEL) private val mediaHost: MediaHost, ) { /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: StateFlow<SceneKey> = @@ -83,12 +77,6 @@ constructor( } } - init { - mediaHost.expansion = MediaHostState.EXPANDED - mediaHost.showsOnlyActiveMedia = true - mediaHost.init(MediaHierarchyManager.LOCATION_QQS) - } - fun isMediaVisible(): Boolean { // TODO(b/296122467): handle updates to carousel visibility while scene is still visible return mediaDataManager.hasActiveMediaOrRecommendation() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java deleted file mode 100644 index cfbd015f1cd8..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.statusbar; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.Log; -import android.view.accessibility.AccessibilityEvent; - -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; -import com.android.systemui.statusbar.policy.HeadsUpManagerLogger; -import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.time.SystemClock; - -import java.util.stream.Stream; - -/** - * A manager which contains notification alerting functionality, providing methods to add and - * remove notifications that appear on screen for a period of time and dismiss themselves at the - * appropriate time. These include heads up notifications and ambient pulses. - */ -public abstract class AlertingNotificationManager { - private static final String TAG = "AlertNotifManager"; - protected final SystemClock mSystemClock; - protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>(); - protected final HeadsUpManagerLogger mLogger; - - protected int mMinimumDisplayTime; - protected int mStickyForSomeTimeAutoDismissTime; - protected int mAutoDismissTime; - private DelayableExecutor mExecutor; - - public AlertingNotificationManager(HeadsUpManagerLogger logger, - SystemClock systemClock, @Main DelayableExecutor executor) { - mLogger = logger; - mExecutor = executor; - mSystemClock = systemClock; - } - - /** - * Called when posting a new notification that should alert the user and appear on screen. - * Adds the notification to be managed. - * @param entry entry to show - */ - public void showNotification(@NonNull NotificationEntry entry) { - mLogger.logShowNotification(entry); - addAlertEntry(entry); - updateNotification(entry.getKey(), true /* alert */); - entry.setInterruption(); - } - - /** - * Try to remove the notification. May not succeed if the notification has not been shown long - * enough and needs to be kept around. - * @param key the key of the notification to remove - * @param releaseImmediately force a remove regardless of earliest removal time - * @return true if notification is removed, false otherwise - */ - public boolean removeNotification(@NonNull String key, boolean releaseImmediately) { - mLogger.logRemoveNotification(key, releaseImmediately); - AlertEntry alertEntry = mAlertEntries.get(key); - if (alertEntry == null) { - return true; - } - if (releaseImmediately || canRemoveImmediately(key)) { - removeAlertEntry(key); - } else { - alertEntry.removeAsSoonAsPossible(); - return false; - } - return true; - } - - /** - * Called when the notification state has been updated. - * @param key the key of the entry that was updated - * @param alert whether the notification should alert again and force reevaluation of - * removal time - */ - public void updateNotification(@NonNull String key, boolean alert) { - AlertEntry alertEntry = mAlertEntries.get(key); - mLogger.logUpdateNotification(key, alert, alertEntry != null); - if (alertEntry == null) { - // the entry was released before this update (i.e by a listener) This can happen - // with the groupmanager - return; - } - - alertEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - if (alert) { - alertEntry.updateEntry(true /* updatePostTime */, "updateNotification"); - } - } - - /** - * Clears all managed notifications. - */ - public void releaseAllImmediately() { - mLogger.logReleaseAllImmediately(); - // A copy is necessary here as we are changing the underlying map. This would cause - // undefined behavior if we iterated over the key set directly. - ArraySet<String> keysToRemove = new ArraySet<>(mAlertEntries.keySet()); - for (String key : keysToRemove) { - removeAlertEntry(key); - } - } - - /** - * Returns the entry if it is managed by this manager. - * @param key key of notification - * @return the entry - */ - @Nullable - public NotificationEntry getEntry(@NonNull String key) { - AlertEntry entry = mAlertEntries.get(key); - return entry != null ? entry.mEntry : null; - } - - /** - * Returns the stream of all current notifications managed by this manager. - * @return all entries - */ - @NonNull - public Stream<NotificationEntry> getAllEntries() { - return mAlertEntries.values().stream().map(headsUpEntry -> headsUpEntry.mEntry); - } - - /** - * Whether or not there are any active alerting notifications. - * @return true if there is an alert, false otherwise - */ - public boolean hasNotifications() { - return !mAlertEntries.isEmpty(); - } - - /** - * Whether or not the given notification is alerting and managed by this manager. - * @return true if the notification is alerting - */ - public boolean isAlerting(@NonNull String key) { - return mAlertEntries.containsKey(key); - } - - /** - * Gets the flag corresponding to the notification content view this alert manager will show. - * - * @return flag corresponding to the content view - */ - public abstract @InflationFlag int getContentFlag(); - - /** - * Add a new entry and begin managing it. - * @param entry the entry to add - */ - protected final void addAlertEntry(@NonNull NotificationEntry entry) { - AlertEntry alertEntry = createAlertEntry(); - alertEntry.setEntry(entry); - mAlertEntries.put(entry.getKey(), alertEntry); - onAlertEntryAdded(alertEntry); - entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - entry.setIsAlerting(true); - } - - /** - * Manager-specific logic that should occur when an entry is added. - * @param alertEntry alert entry added - */ - protected abstract void onAlertEntryAdded(@NonNull AlertEntry alertEntry); - - /** - * Remove a notification and reset the alert entry. - * @param key key of notification to remove - */ - protected final void removeAlertEntry(@NonNull String key) { - AlertEntry alertEntry = mAlertEntries.get(key); - if (alertEntry == null) { - return; - } - NotificationEntry entry = alertEntry.mEntry; - - // If the notification is animating, we will remove it at the end of the animation. - if (entry != null && entry.isExpandAnimationRunning()) { - return; - } - entry.demoteStickyHun(); - mAlertEntries.remove(key); - onAlertEntryRemoved(alertEntry); - entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - alertEntry.reset(); - } - - /** - * Manager-specific logic that should occur when an alert entry is removed. - * @param alertEntry alert entry removed - */ - protected abstract void onAlertEntryRemoved(@NonNull AlertEntry alertEntry); - - /** - * Returns a new alert entry instance. - * @return a new AlertEntry - */ - protected AlertEntry createAlertEntry() { - return new AlertEntry(); - } - - /** - * Whether or not the alert can be removed currently. If it hasn't been on screen long enough - * it should not be removed unless forced - * @param key the key to check if removable - * @return true if the alert entry can be removed - */ - public boolean canRemoveImmediately(String key) { - AlertEntry alertEntry = mAlertEntries.get(key); - return alertEntry == null || alertEntry.wasShownLongEnough() - || alertEntry.mEntry.isRowDismissed(); - } - - /** - * @param key - * @return true if the entry is (pinned and expanded) or (has an active remote input) - */ - public boolean isSticky(String key) { - AlertEntry alerting = mAlertEntries.get(key); - if (alerting != null) { - return alerting.isSticky(); - } - return false; - } - - /** - * @param key - * @return When a HUN entry should be removed in milliseconds from now - */ - public long getEarliestRemovalTime(String key) { - AlertEntry alerting = mAlertEntries.get(key); - if (alerting != null) { - return Math.max(0, alerting.mEarliestRemovalTime - mSystemClock.elapsedRealtime()); - } - return 0; - } - - protected class AlertEntry implements Comparable<AlertEntry> { - @Nullable public NotificationEntry mEntry; - public long mPostTime; - public long mEarliestRemovalTime; - - @Nullable protected Runnable mRemoveAlertRunnable; - @Nullable private Runnable mCancelRemoveAlertRunnable; - - public void setEntry(@NonNull final NotificationEntry entry) { - setEntry(entry, () -> removeAlertEntry(entry.getKey())); - } - - public void setEntry(@NonNull final NotificationEntry entry, - @Nullable Runnable removeAlertRunnable) { - mEntry = entry; - mRemoveAlertRunnable = removeAlertRunnable; - - mPostTime = calculatePostTime(); - updateEntry(true /* updatePostTime */, "setEntry"); - } - - /** - * Updates an entry's removal time. - * @param updatePostTime whether or not to refresh the post time - */ - public void updateEntry(boolean updatePostTime, @Nullable String reason) { - mLogger.logUpdateEntry(mEntry, updatePostTime, reason); - - final long now = mSystemClock.elapsedRealtime(); - mEarliestRemovalTime = now + mMinimumDisplayTime; - - if (updatePostTime) { - mPostTime = Math.max(mPostTime, now); - } - - if (isSticky()) { - removeAutoRemovalCallbacks("updateEntry (sticky)"); - return; - } - - final long finishTime = calculateFinishTime(); - final long timeLeft = Math.max(finishTime - now, mMinimumDisplayTime); - scheduleAutoRemovalCallback(timeLeft, "updateEntry (not sticky)"); - } - - /** - * Whether or not the notification is "sticky" i.e. should stay on screen regardless - * of the timer (forever) and should be removed externally. - * @return true if the notification is sticky - */ - public boolean isSticky() { - // This implementation is overridden by HeadsUpManager HeadsUpEntry #isSticky - return false; - } - - public boolean isStickyForSomeTime() { - // This implementation is overridden by HeadsUpManager HeadsUpEntry #isStickyForSomeTime - return false; - } - - /** - * Whether the notification has befen on screen long enough and can be removed. - * @return true if the notification has been on screen long enough - */ - public boolean wasShownLongEnough() { - return mEarliestRemovalTime < mSystemClock.elapsedRealtime(); - } - - @Override - public int compareTo(@NonNull AlertEntry alertEntry) { - return (mPostTime < alertEntry.mPostTime) - ? 1 : ((mPostTime == alertEntry.mPostTime) - ? mEntry.getKey().compareTo(alertEntry.mEntry.getKey()) : -1); - } - - public void reset() { - removeAutoRemovalCallbacks("reset()"); - mEntry = null; - mRemoveAlertRunnable = null; - } - - /** - * Clear any pending removal runnables. - */ - public void removeAutoRemovalCallbacks(@Nullable String reason) { - final boolean removed = removeAutoRemovalCallbackInternal(); - - if (removed) { - mLogger.logAutoRemoveCanceled(mEntry, reason); - } - } - - private void scheduleAutoRemovalCallback(long delayMillis, @NonNull String reason) { - if (mRemoveAlertRunnable == null) { - Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set"); - return; - } - - final boolean removed = removeAutoRemovalCallbackInternal(); - - if (removed) { - mLogger.logAutoRemoveRescheduled(mEntry, delayMillis, reason); - } else { - mLogger.logAutoRemoveScheduled(mEntry, delayMillis, reason); - } - - - mCancelRemoveAlertRunnable = mExecutor.executeDelayed(mRemoveAlertRunnable, - delayMillis); - } - - private boolean removeAutoRemovalCallbackInternal() { - final boolean scheduled = (mCancelRemoveAlertRunnable != null); - - if (scheduled) { - mCancelRemoveAlertRunnable.run(); - mCancelRemoveAlertRunnable = null; - } - - return scheduled; - } - - /** - * Remove the alert at the earliest allowed removal time. - */ - public void removeAsSoonAsPossible() { - if (mRemoveAlertRunnable != null) { - final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime(); - scheduleAutoRemovalCallback(timeLeft, "removeAsSoonAsPossible"); - } - } - - /** - * Calculate what the post time of a notification is at some current time. - * @return the post time - */ - protected long calculatePostTime() { - return mSystemClock.elapsedRealtime(); - } - - /** - * @return When the notification should auto-dismiss itself, based on - * {@link SystemClock#elapsedRealtime()} - */ - protected long calculateFinishTime() { - // Overridden by HeadsUpManager HeadsUpEntry #calculateFinishTime - return 0; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 7a2e82f24f46..bb6ee24b0ffe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -645,6 +645,10 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi + "slot='" + mSlot + "' alpha=" + getAlpha() + " icon=" + mIcon + " visibleState=" + getVisibleStateString(getVisibleState()) + " iconColor=#" + Integer.toHexString(mIconColor) + + " staticDrawableColor=#" + Integer.toHexString(mDrawableColor) + + " decorColor=#" + Integer.toHexString(mDecorColor) + + " animationStartColor=#" + Integer.toHexString(mAnimationStartColor) + + " currentSetColor=#" + Integer.toHexString(mCurrentSetColor) + " notification=" + mNotification + ')'; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index 96279e2d2e44..5983fc17d4c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -157,9 +157,9 @@ class NotificationLaunchAnimatorController( val summaryEntry = notificationEntry.parent?.summary return when { - headsUpManager.isAlerting(notificationKey) -> notification + headsUpManager.isHeadsUpEntry(notificationKey) -> notification summaryEntry == null -> null - headsUpManager.isAlerting(summaryEntry.key) -> summaryEntry.row + headsUpManager.isHeadsUpEntry(summaryEntry.key) -> summaryEntry.row else -> null } } 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 cfe9fbe3af29..cdacb10e1676 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 @@ -171,7 +171,7 @@ public final class NotificationEntry extends ListEntry { private int mBucket = BUCKET_ALERTING; @Nullable private Long mPendingAnimationDuration; private boolean mIsMarkedForUserTriggeredMovement; - private boolean mIsAlerting; + private boolean mIsHeadsUpEntry; public boolean mRemoteEditImeAnimatingAway; public boolean mRemoteEditImeVisible; @@ -965,12 +965,12 @@ public final class NotificationEntry extends ListEntry { mIsMarkedForUserTriggeredMovement = marked; } - public void setIsAlerting(boolean isAlerting) { - mIsAlerting = isAlerting; + public void setIsHeadsUpEntry(boolean isHeadsUpEntry) { + mIsHeadsUpEntry = isHeadsUpEntry; } - public boolean isAlerting() { - return mIsAlerting; + public boolean isHeadsUpEntry() { + return mIsHeadsUpEntry; } /** Set whether this notification is currently used to animate a launch. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 314566eaf8d0..46806e66cb8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -221,7 +221,7 @@ class HeadsUpCoordinator @Inject constructor( wasUpdated = false, shouldHeadsUpEver = false, shouldHeadsUpAgain = false, - isAlerting = mHeadsUpManager.isAlerting(logicalSummary.key), + isAlerting = mHeadsUpManager.isHeadsUpEntry(logicalSummary.key), isBinding = isEntryBinding(logicalSummary), ) // If we transfer the alert and the summary isn't even attached, that means we @@ -268,7 +268,7 @@ class HeadsUpCoordinator @Inject constructor( wasUpdated = false, shouldHeadsUpEver = true, shouldHeadsUpAgain = true, - isAlerting = mHeadsUpManager.isAlerting(childToReceiveParentAlert.key), + isAlerting = mHeadsUpManager.isHeadsUpEntry(childToReceiveParentAlert.key), isBinding = isEntryBinding(childToReceiveParentAlert), ) handlePostedEntry( @@ -425,7 +425,7 @@ class HeadsUpCoordinator @Inject constructor( val shouldHeadsUpEver = mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt val shouldHeadsUpAgain = shouldHunAgain(entry) - val isAlerting = mHeadsUpManager.isAlerting(entry.key) + val isAlerting = mHeadsUpManager.isHeadsUpEntry(entry.key) val isBinding = isEntryBinding(entry) val posted = mPostedEntries.compute(entry.key) { _, value -> value?.also { update -> @@ -469,7 +469,7 @@ class HeadsUpCoordinator @Inject constructor( mEntriesUpdateTimes.remove(entry.key) cancelHeadsUpBind(entry) val entryKey = entry.key - if (mHeadsUpManager.isAlerting(entryKey)) { + if (mHeadsUpManager.isHeadsUpEntry(entryKey)) { // TODO: This should probably know the RemoteInputCoordinator's conditions, // or otherwise reference that coordinator's state, rather than replicate its logic val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) && @@ -719,7 +719,7 @@ class HeadsUpCoordinator @Inject constructor( * Whether the notification is already alerting or binding so that it can imminently alert */ private fun isAttemptingToShowHun(entry: ListEntry) = - mHeadsUpManager.isAlerting(entry.key) || isEntryBinding(entry) + mHeadsUpManager.isHeadsUpEntry(entry.key) || isEntryBinding(entry) /** * Whether the notification is already alerting/binding per [isAttemptingToShowHun] OR if it 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 a0129ff5cd90..8189fe03b2ed 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 @@ -134,7 +134,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { private final NotifStabilityManager mNotifStabilityManager = new NotifStabilityManager("VisualStabilityCoordinator") { private boolean canMoveForHeadsUp(NotificationEntry entry) { - return entry != null && mHeadsUpManager.isAlerting(entry.getKey()) + return entry != null && mHeadsUpManager.isHeadsUpEntry(entry.getKey()) && !mVisibilityLocationProvider.isInVisibleLocation(entry); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java index e71d80c130da..5743ab0eae27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java @@ -68,7 +68,7 @@ public class OnUserInteractionCallbackImpl implements OnUserInteractionCallback @NonNull private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) { int dismissalSurface = NotificationStats.DISMISSAL_SHADE; - if (mHeadsUpManager.isAlerting(entry.getKey())) { + if (mHeadsUpManager.isHeadsUpEntry(entry.getKey())) { dismissalSurface = NotificationStats.DISMISSAL_PEEK; } else if (mStatusBarStateController.isDozing()) { dismissalSurface = NotificationStats.DISMISSAL_AOD; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index 5180bab60fdf..b22e9fd2fb17 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -68,7 +68,8 @@ constructor( /** * The same as [allNotificationsCount], but without flows, for easy access in synchronous code. */ - val allNotificationsCountValue: Int = repository.activeNotifications.value.individuals.size + val allNotificationsCountValue: Int + get() = repository.activeNotifications.value.individuals.size /** Are any notifications being actively presented in the notification stack? */ val areAnyNotificationsPresent: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index ac499601b962..1677418c5c30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener @@ -72,7 +71,6 @@ constructor( private val animatedImageNotificationManager: AnimatedImageNotificationManager, private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager, private val bubblesOptional: Optional<Bubbles>, - private val featureFlags: FeatureFlags ) : NotificationsController { override fun initialize( @@ -136,5 +134,8 @@ constructor( } } - override fun getActiveNotificationsCount(): Int = notifLiveDataStore.activeNotifCount.value + override fun getActiveNotificationsCount(): Int { + NotificationsLiveDataStoreRefactor.assertInLegacyMode() + return notifLiveDataStore.activeNotifCount.value + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncGroupHeaderViewInflation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncGroupHeaderViewInflation.kt new file mode 100644 index 000000000000..44fc77f8bd55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncGroupHeaderViewInflation.kt @@ -0,0 +1,53 @@ +/* + * 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.row.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the Async Group Header Inflation flag state. */ +@Suppress("NOTHING_TO_INLINE") +object AsyncGroupHeaderViewInflation { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the async inflation of group header views enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationAsyncGroupHeaderInflation() + + /** + * 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/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index d9c51089d5f8..20fae88b6f33 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -26,9 +26,9 @@ import android.util.MathUtils; import androidx.annotation.VisibleForTesting; import com.android.systemui.Dumpable; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.res.R; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; @@ -578,7 +578,7 @@ public class AmbientState implements Dumpable { } public boolean isPulsing(NotificationEntry entry) { - return mPulsing && entry.isAlerting(); + return mPulsing && entry.isHeadsUpEntry(); } public void setPulsingRow(ExpandableNotificationRow row) { 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 e791a6490e15..04db653282ac 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 @@ -94,6 +94,7 @@ import com.android.systemui.flags.RefactorFlag; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; @@ -187,6 +188,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mOverflingDistance; private float mMaxOverScroll; private boolean mIsBeingDragged; + private boolean mSendingTouchesToSceneFramework; private int mLastMotionY; private int mDownX; private int mActivePointerId = INVALID_POINTER; @@ -1509,7 +1511,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ public void setExpandedHeight(float height) { final boolean skipHeightUpdate = shouldSkipHeightUpdate(); - updateStackPosition(); + + // when scene framework is enabled, updateStackPosition is already called by + // updateTopPadding every time the stack moves, so skip it here to avoid flickering. + if (!SceneContainerFlag.isEnabled()) { + updateStackPosition(); + } if (!skipHeightUpdate) { mExpandedHeight = height; @@ -2450,6 +2457,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /* notificationStackScrollLayout= */ this, mMaxDisplayedNotifications, shelfIntrinsicHeight); mIntrinsicContentHeight = height; + mController.setIntrinsicContentHeight(mIntrinsicContentHeight); // The topPadding can be bigger than the regular padding when qs is expanded, in that // state the maxPanelHeight and the contentHeight should be bigger @@ -3558,8 +3566,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public boolean onTouchEvent(MotionEvent ev) { - if (mTouchHandler != null && mTouchHandler.onTouchEvent(ev)) { - return true; + if (mTouchHandler != null) { + boolean touchHandled = mTouchHandler.onTouchEvent(ev); + if (SceneContainerFlag.isEnabled() || touchHandled) { + return touchHandled; + } } return super.onTouchEvent(ev); @@ -3567,6 +3578,27 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public boolean dispatchTouchEvent(MotionEvent ev) { + if (SceneContainerFlag.isEnabled() && mIsBeingDragged) { + if (!mSendingTouchesToSceneFramework) { + // if this is the first touch being sent to the scene framework, + // convert it into a synthetic DOWN event. + mSendingTouchesToSceneFramework = true; + MotionEvent downEvent = MotionEvent.obtain(ev); + downEvent.setAction(MotionEvent.ACTION_DOWN); + mController.sendTouchToSceneFramework(downEvent); + downEvent.recycle(); + } else { + mController.sendTouchToSceneFramework(ev); + } + + if ( + ev.getActionMasked() == MotionEvent.ACTION_UP + || ev.getActionMasked() == MotionEvent.ACTION_CANCEL + ) { + setIsBeingDragged(false); + } + return false; + } return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev)); } @@ -3633,6 +3665,18 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return true; } + // If the scene framework is enabled, ignore all non-move gestures if we are currently + // dragging - they should be dispatched to the scene framework. Move gestures should be let + // through to determine if we are still dragging or not. + if ( + SceneContainerFlag.isEnabled() + && mIsBeingDragged + && action != MotionEvent.ACTION_MOVE + ) { + setIsBeingDragged(false); + return false; + } + switch (action) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0 || !isInContentBounds(ev)) { @@ -3676,6 +3720,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } if (mIsBeingDragged) { + // Defer actual scrolling to the scene framework if enabled + if (SceneContainerFlag.isEnabled()) { + setIsBeingDragged(false); + return false; + } // Scroll to follow the motion event mLastMotionY = y; float scrollAmount; @@ -3770,9 +3819,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } protected boolean isInsideQsHeader(MotionEvent ev) { - if (mQsHeader == null) { - Log.wtf(TAG, "qsHeader is null while NSSL is handling a touch"); - return false; + if (SceneContainerFlag.isEnabled()) { + return ev.getY() < mController.getPlaceholderTop(); } mQsHeader.getBoundsOnScreen(mQsHeaderBound); @@ -4026,9 +4074,16 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable requestDisallowInterceptTouchEvent(true); cancelLongPress(); resetExposedMenuView(true /* animate */, true /* force */); + } else { + mSendingTouchesToSceneFramework = false; } } + @VisibleForTesting + boolean getIsBeingDragged() { + return mIsBeingDragged; + } + public void requestDisallowLongPress() { cancelLongPress(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index c0e0b73c750d..6a66bb74f16d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -80,6 +80,9 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEv import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; +import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; +import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.CommandQueue; @@ -120,6 +123,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; +import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; @@ -145,6 +149,7 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Provider; /** * Controller for {@link NotificationStackScrollLayout}. @@ -181,6 +186,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationRemoteInputManager mRemoteInputManager; private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; private final ShadeController mShadeController; + private final Provider<WindowRootView> mWindowRootView; + private final NotificationStackAppearanceInteractor mStackAppearanceInteractor; private final KeyguardMediaController mKeyguardMediaController; private final SysuiStatusBarStateController mStatusBarStateController; private final KeyguardBypassController mKeyguardBypassController; @@ -689,6 +696,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { SeenNotificationsInteractor seenNotificationsInteractor, NotificationListViewBinder viewBinder, ShadeController shadeController, + SceneContainerFlags sceneContainerFlags, + Provider<WindowRootView> windowRootView, + NotificationStackAppearanceInteractor stackAppearanceInteractor, InteractionJankMonitor jankMonitor, StackStateLogger stackLogger, NotificationStackScrollLogger logger, @@ -739,6 +749,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator; mSeenNotificationsInteractor = seenNotificationsInteractor; mShadeController = shadeController; + mWindowRootView = windowRootView; + mStackAppearanceInteractor = stackAppearanceInteractor; mFeatureFlags = featureFlags; mNotificationTargetsHelper = notificationTargetsHelper; mSecureSettings = secureSettings; @@ -1076,6 +1088,28 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView.getIntrinsicContentHeight(); } + /** + * Dispatch a touch to the scene container framework. + * TODO(b/316965302): Replace findViewById to avoid DFS + */ + public void sendTouchToSceneFramework(MotionEvent ev) { + View sceneContainer = mWindowRootView.get() + .findViewById(R.id.scene_container_root_composable); + if (sceneContainer != null) { + sceneContainer.dispatchTouchEvent(ev); + } + } + + /** Get the y-coordinate of the top bound of the stack. */ + public float getPlaceholderTop() { + return mStackAppearanceInteractor.getStackBounds().getValue().getTop(); + } + + /** Set the intrinsic height of the stack content without additional padding. */ + public void setIntrinsicContentHeight(float intrinsicContentHeight) { + mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight); + } + public void setIntrinsicPadding(int intrinsicPadding) { mView.setIntrinsicPadding(intrinsicPadding); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index e78a694735e0..aac3c28a3426 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -30,4 +30,18 @@ class NotificationStackAppearanceRepository @Inject constructor() { /** The corner radius of the notification stack, in dp. */ val cornerRadiusDp = MutableStateFlow(32f) + + /** + * The height in px of the contents of notification stack. Depending on the number of + * notifications, this can exceed the space available on screen to show notifications, at which + * point the notification stack should become scrollable. + */ + val intrinsicContentHeight = MutableStateFlow(0f) + + /** + * The y-coordinate in px of top of the contents of the notification stack. This value can be + * negative, if the stack is scrolled such that its top extends beyond the top edge of the + * screen. + */ + val contentTop = MutableStateFlow(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 61a4dfcbd201..1dfde09f3a85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -34,12 +34,32 @@ constructor( /** The bounds of the notification stack in the current scene. */ val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow() + /** The corner radius of the notification stack, in dp. */ + val cornerRadiusDp: StateFlow<Float> = repository.cornerRadiusDp.asStateFlow() + + /** + * The height in px of the contents of notification stack. Depending on the number of + * notifications, this can exceed the space available on screen to show notifications, at which + * point the notification stack should become scrollable. + */ + val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow() + + /** The y-coordinate in px of top of the contents of the notification stack. */ + val contentTop = repository.contentTop.asStateFlow() + /** Sets the position of the notification stack in the current scene. */ fun setStackBounds(bounds: NotificationContainerBounds) { check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } repository.stackBounds.value = bounds } - /** The corner radius of the notification stack, in dp. */ - val cornerRadiusDp: StateFlow<Float> = repository.cornerRadiusDp.asStateFlow() + /** Sets the height of the contents of the notification stack. */ + fun setIntrinsicContentHeight(height: Float) { + repository.intrinsicContentHeight.value = height + } + + /** Sets the y-coord in px of the top of the contents of the notification stack. */ + fun setContentTop(startY: Float) { + repository.contentTop.value = startY + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt index a9b542dcce2d..ed15f557fb39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt @@ -25,6 +25,7 @@ import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import kotlin.math.pow import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -43,24 +44,28 @@ object NotificationStackAppearanceViewBinder { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.stackBounds.collect { bounds -> - controller.updateTopPadding( - bounds.top, - controller.isAddOrRemoveAnimationPending - ) controller.setRoundedClippingBounds( - it.left, - it.top, - it.right, - it.bottom, + bounds.left.roundToInt(), + bounds.top.roundToInt(), + bounds.right.roundToInt(), + bounds.bottom.roundToInt(), viewModel.cornerRadiusDp.value.dpToPx(context), viewModel.cornerRadiusDp.value.dpToPx(context), ) } } + + launch { + viewModel.contentTop.collect { + controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + } + } + launch { viewModel.expandFraction.collect { expandFraction -> ambientState.expansionFraction = expandFraction controller.expandedHeight = expandFraction * controller.view.height + controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f)) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index 834d3ffe63c9..74db5831f7f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -41,4 +41,7 @@ constructor( /** The corner radius of the notification stack, in dp. */ val cornerRadiusDp: StateFlow<Float> = stackAppearanceInteractor.cornerRadiusDp + + /** The y-coordinate in px of top of the contents of the notification stack. */ + val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 9f22118e3332..385f0619288d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -21,9 +21,11 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -35,6 +37,7 @@ class NotificationsPlaceholderViewModel @Inject constructor( private val interactor: NotificationStackAppearanceInteractor, + shadeInteractor: ShadeInteractor, flags: SceneContainerFlags, featureFlags: FeatureFlagsClassic, ) { @@ -66,4 +69,22 @@ constructor( /** The corner radius of the placeholder, in dp. */ val cornerRadiusDp: StateFlow<Float> = interactor.cornerRadiusDp + + /** + * The height in px of the contents of notification stack. Depending on the number of + * notifications, this can exceed the space available on screen to show notifications, at which + * point the notification stack should become scrollable. + */ + val intrinsicContentHeight = interactor.intrinsicContentHeight + + /** + * The amount [0-1] that the shade has been opened. At 0, the shade is closed; at 1, the shade + * is open. + */ + val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion + + /** Sets the y-coord in px of the top of the contents of the notification stack. */ + fun onContentTopChanged(padding: Float) { + interactor.setContentTop(padding) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 5ee38bebc99b..a48fb45861d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -240,13 +240,13 @@ constructor( */ val translationY: Flow<Float> = combine( - isOnLockscreen, + isOnLockscreenWithoutShade, merge( keyguardInteractor.keyguardTranslationY, occludedToLockscreenTransitionViewModel.lockscreenTranslationY, ) - ) { isOnLockscreen, translationY -> - if (isOnLockscreen) { + ) { isOnLockscreenWithoutShade, translationY -> + if (isOnLockscreenWithoutShade) { translationY } else { 0f 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 e66d9e8ca531..b07ba3c5b326 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -222,9 +222,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp mReleaseOnExpandFinish = false; } else { for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { - if (isAlerting(entry.getKey())) { + if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already - removeAlertEntry(entry.getKey()); + removeEntry(entry.getKey()); } } } @@ -357,9 +357,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { - if (isAlerting(entry.getKey())) { + if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already - removeAlertEntry(entry.getKey()); + removeEntry(entry.getKey()); } } mEntriesToRemoveWhenReorderingAllowed.clear(); @@ -370,14 +370,14 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp // HeadsUpManager utility (protected) methods overrides: @Override - protected HeadsUpEntry createAlertEntry() { + protected HeadsUpEntry createHeadsUpEntry() { return mEntryPool.acquire(); } @Override - protected void onAlertEntryRemoved(AlertEntry alertEntry) { - super.onAlertEntryRemoved(alertEntry); - mEntryPool.release((HeadsUpEntryPhone) alertEntry); + protected void onEntryRemoved(HeadsUpEntry headsUpEntry) { + super.onEntryRemoved(headsUpEntry); + mEntryPool.release((HeadsUpEntryPhone) headsUpEntry); } @Override @@ -403,7 +403,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Nullable private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { - return (HeadsUpEntryPhone) mAlertEntries.get(key); + return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key); } @Nullable @@ -455,7 +455,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp } else if (mTrackingHeadsUp) { mEntriesToRemoveAfterExpand.add(entry); } else { - removeAlertEntry(entry.getKey()); + removeEntry(entry.getKey()); } }; @@ -529,13 +529,13 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp mStatusBarState = newState; if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) { ArrayList<String> keysToRemove = new ArrayList<>(); - for (AlertEntry entry : mAlertEntries.values()) { + for (HeadsUpEntry entry : mHeadsUpEntryMap.values()) { if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) { keysToRemove.add(entry.mEntry.getKey()); } } for (String key : keysToRemove) { - removeAlertEntry(key); + removeEntry(key); } } } @@ -545,7 +545,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp if (!isDozing) { // Let's make sure all huns we got while dozing time out within the normal timeout // duration. Otherwise they could get stuck for a very long time - for (AlertEntry entry : mAlertEntries.values()) { + for (HeadsUpEntry entry : mHeadsUpEntryMap.values()) { entry.updateEntry(true /* updatePostTime */, "onDozingChanged(false)"); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index f34a44a5c4b0..be5c6b3ba069 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -918,8 +918,15 @@ public class NotificationIconContainer extends ViewGroup { } } icon.setVisibleState(visibleState, animationsAllowed); - icon.setIconColor(mOverrideIconColor ? mThemedTextColorPrimary : iconColor, - needsCannedAnimation && animationsAllowed); + if (NotificationIconContainerRefactor.isEnabled()) { + if (mOverrideIconColor) { + icon.setIconColor(mThemedTextColorPrimary, + /* animate= */ needsCannedAnimation && animationsAllowed); + } + } else { + icon.setIconColor(mOverrideIconColor ? mThemedTextColorPrimary : iconColor, + needsCannedAnimation && animationsAllowed); + } if (animate) { animateTo(icon, animationProperties); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 9da61112fd0c..4ee061d05a3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -562,7 +562,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private void removeHunAfterClick(ExpandableNotificationRow row) { String key = row.getEntry().getSbn().getKey(); - if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(key)) { + if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUpEntry(key)) { // Release the HUN notification to the shade. if (mPresenter.isPresenterFullyCollapsed()) { HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(row, true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 2b90e649a154..e309c32df64e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -265,13 +265,6 @@ abstract class StatusBarPipelineModule { return factory.create("VerboseMobileViewLog", 100) } - @Provides - @SysUISingleton - @OemSatelliteInputLog - fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer { - return factory.create("DeviceBasedSatelliteInputLog", 32) - } - const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON = "FirstMobileSubShowingNetworkTypeIcon" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt index 8400fb08e147..e3c3139f6906 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.pipeline.icons.shared import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon -import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon import javax.inject.Inject /** @@ -39,12 +38,11 @@ interface BindableIconsRegistry { class BindableIconsRegistryImpl @Inject constructor( - /** Bindables go here */ - oemSatellite: DeviceBasedSatelliteBindableIcon +/** Bindables go here */ ) : BindableIconsRegistry { /** * Adding the injected bindables to this list will get them registered with * StatusBarIconController */ - override val bindableIcons: List<BindableIcon> = listOf(oemSatellite) + override val bindableIcons: List<BindableIcon> = listOf() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index 5e6e36dd69a9..de46a5ed99d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -19,17 +19,11 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager -import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS import android.telephony.satellite.SatelliteModemStateCallback import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.core.LogLevel -import com.android.systemui.log.core.MessageInitializer -import com.android.systemui.log.core.MessagePrinter -import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported @@ -129,7 +123,6 @@ constructor( satelliteManagerOpt: Optional<SatelliteManager>, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, - @OemSatelliteInputLog private val logBuffer: LogBuffer, private val systemClock: SystemClock, ) : DeviceBasedSatelliteRepository { @@ -152,11 +145,6 @@ constructor( ensureMinUptime(systemClock, MIN_UPTIME) satelliteSupport.value = satelliteManager.checkSatelliteSupported() - logBuffer.i( - { str1 = satelliteSupport.value.toString() }, - { "Checked for system support. support=$str1" }, - ) - // We only need to check location availability if this mode is supported if (satelliteSupport.value is Supported) { isSatelliteAllowedForCurrentLocation.subscriptionCount @@ -171,9 +159,6 @@ constructor( * connection might cause more frequent checks. */ while (true) { - logBuffer.i { - "requestIsSatelliteCommunicationAllowedForCurrentLocation" - } checkIsSatelliteAllowed() delay(POLLING_INTERVAL_MS) } @@ -182,8 +167,6 @@ constructor( } } } else { - logBuffer.i { "Satellite manager is null" } - satelliteSupport.value = NotSupported } } @@ -198,21 +181,12 @@ constructor( private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = conflatedCallbackFlow { val cb = SatelliteModemStateCallback { state -> - logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" } trySend(SatelliteConnectionState.fromModemState(state)) } - var registered = false - - try { - val res = - sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) - registered = res == SATELLITE_RESULT_SUCCESS - } catch (e: Exception) { - logBuffer.e("error registering for modem state", e) - } + sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) - awaitClose { if (registered) sm.unregisterForSatelliteModemStateChanged(cb) } + awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) } } .flowOn(bgDispatcher) @@ -223,21 +197,12 @@ constructor( private fun signalStrengthFlow(sm: SupportedSatelliteManager) = conflatedCallbackFlow { val cb = NtnSignalStrengthCallback { signalStrength -> - logBuffer.i({ int1 = signalStrength.level }) { - "onNtnSignalStrengthChanged: level=$int1" - } trySend(signalStrength.level) } - var registered = false - try { - sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) - registered = true - } catch (e: Exception) { - logBuffer.e("error registering for signal strength", e) - } + sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) - awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) } + awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) } } .flowOn(bgDispatcher) @@ -248,15 +213,11 @@ constructor( bgDispatcher.asExecutor(), object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { override fun onError(e: SatelliteManager.SatelliteException) { - logBuffer.e( - "Found exception when checking availability", - e, - ) + android.util.Log.e(TAG, "Found exception when checking for satellite: ", e) isSatelliteAllowedForCurrentLocation.value = false } override fun onResult(allowed: Boolean) { - logBuffer.i { allowed.toString() } isSatelliteAllowedForCurrentLocation.value = allowed } } @@ -278,12 +239,6 @@ constructor( } override fun onError(error: SatelliteManager.SatelliteException) { - logBuffer.e( - "Exception when checking for satellite support. " + - "Assuming it is not supported for this device.", - error, - ) - // Assume that an error means it's not supported continuation.resume(NotSupported) } @@ -309,19 +264,5 @@ constructor( delay(timeTilMinUptime) } } - - /** A couple of convenience logging methods rather than a whole class */ - private fun LogBuffer.i( - initializer: MessageInitializer = {}, - printer: MessagePrinter, - ) = this.log(TAG, LogLevel.INFO, initializer, printer) - - private fun LogBuffer.e(message: String, exception: Throwable? = null) = - this.log( - tag = TAG, - level = LogLevel.ERROR, - message = message, - exception = exception, - ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt deleted file mode 100644 index f5d0f6b8f07c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui - -import android.content.Context -import com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon -import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator -import com.android.systemui.statusbar.pipeline.satellite.ui.binder.DeviceBasedSatelliteIconBinder -import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView -import javax.inject.Inject - -@SysUISingleton -class DeviceBasedSatelliteBindableIcon -@Inject -constructor( - context: Context, - viewModel: DeviceBasedSatelliteViewModel, -) : BindableIcon { - override val slot: String = - context.getString(com.android.internal.R.string.status_bar_oem_satellite) - - override val initializer = ModernStatusBarViewCreator { context -> - SingleBindableStatusBarIconView.createView(context).also { view -> - view.initView(slot) { DeviceBasedSatelliteIconBinder.bind(view, viewModel) } - } - } - - override val shouldBindIcon: Boolean = oemEnabledSatelliteFlag() -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt deleted file mode 100644 index 59ac5f29b66c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.binder - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.common.ui.binder.IconViewBinder -import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding -import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView -import kotlinx.coroutines.launch - -object DeviceBasedSatelliteIconBinder { - fun bind( - view: SingleBindableStatusBarIconView, - viewModel: DeviceBasedSatelliteViewModel, - ): ModernStatusBarViewBinding { - return SingleBindableStatusBarIconView.withDefaultBinding( - view = view, - shouldBeVisible = { viewModel.icon.value != null } - ) { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.icon.collect { newIcon -> - if (newIcon == null) { - view.iconView.setImageDrawable(null) - } else { - IconViewBinder.bind(newIcon, view.iconView) - } - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt deleted file mode 100644 index 6938d667ca81..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.model - -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.res.R -import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState - -/** - * Define the [Icon] that relates to a given satellite connection state + level. Note that for now - * We don't need any data class box, so we can just use a simple mapping function. - */ -object SatelliteIconModel { - fun fromConnectionState( - connectionState: SatelliteConnectionState, - signalStrength: Int, - ): Icon? = - when (connectionState) { - // TODO(b/316635648): check if this should be null - SatelliteConnectionState.Unknown, - SatelliteConnectionState.Off, - SatelliteConnectionState.On -> - Icon.Resource( - res = R.drawable.ic_satellite_not_connected, - contentDescription = null, - ) - SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength) - } - - private fun fromSignalStrength( - signalStrength: Int, - ): Icon? = - // TODO(b/316634365): these need content descriptions - when (signalStrength) { - // No signal - 0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null) - - // Poor -> Moderate - 1, - 2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null) - - // Good -> Great - 3, - 4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null) - else -> null - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt deleted file mode 100644 index 0051161eff35..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel - -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor -import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn - -/** - * View-Model for the device-based satellite icon. This icon will only show in the status bar if - * satellite is available AND all other service states are considered OOS. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class DeviceBasedSatelliteViewModel -@Inject -constructor( - interactor: DeviceBasedSatelliteInteractor, - @Application scope: CoroutineScope, -) { - private val shouldShowIcon: StateFlow<Boolean> = - interactor.areAllConnectionsOutOfService - .flatMapLatest { allOos -> - if (!allOos) { - flowOf(false) - } else { - interactor.isSatelliteAllowed - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - val icon: StateFlow<Icon?> = - combine( - shouldShowIcon, - interactor.connectionState, - interactor.signalStrength, - ) { shouldShow, state, signalStrength -> - if (shouldShow) { - SatelliteIconModel.fromConnectionState(state, signalStrength) - } else { - null - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt index 25a2c9dd3caf..3b87bed2e0ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt @@ -103,7 +103,7 @@ open class ModernStatusBarView(context: Context, attrs: AttributeSet?) : * * Creates a dot view, and uses [bindingCreator] to get and set the binding. */ - open fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot // view. So, this is the required order. this.slot = slot diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt deleted file mode 100644 index c663c37fec98..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.shared.ui.view - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.ImageView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.res.R -import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN -import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding -import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -/** Simple single-icon view that is bound to bindable_status_bar_icon.xml */ -class SingleBindableStatusBarIconView( - context: Context, - attrs: AttributeSet?, -) : ModernStatusBarView(context, attrs) { - - internal lateinit var iconView: ImageView - internal lateinit var dotView: StatusBarIconView - - override fun toString(): String { - return "SingleBindableStatusBarIcon(" + - "slot='$slot', " + - "isCollecting=${binding.isCollecting()}, " + - "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + - "viewString=${super.toString()}" - } - - override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { - super.initView(slot, bindingCreator) - - iconView = requireViewById(R.id.icon_view) - dotView = requireViewById(R.id.status_bar_dot) - } - - companion object { - fun createView( - context: Context, - ): SingleBindableStatusBarIconView { - return LayoutInflater.from(context).inflate(R.layout.bindable_status_bar_icon, null) - as SingleBindableStatusBarIconView - } - - /** - * Using a given binding [block], create the necessary scaffolding to handle the general - * case of a single status bar icon. This includes eliding into a dot view when there is not - * enough space, and handling tint. - * - * [block] should be a simple [launch] call that handles updating the single icon view with - * its new view. Currently there is no simple way to e.g., extend to handle multiple tints - * for dual-layered icons, and any more complex logic should probably find a way to return - * its own version of [ModernStatusBarViewBinding]. - */ - fun withDefaultBinding( - view: SingleBindableStatusBarIconView, - shouldBeVisible: () -> Boolean, - block: suspend LifecycleOwner.(View) -> Unit - ): SingleBindableStatusBarIconViewBinding { - @StatusBarIconView.VisibleState - val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) - - val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) - val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) - - var isCollecting: Boolean = false - - view.repeatWhenAttached { - // Child binding - block(view) - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - // isVisible controls the visibility state of the outer group, and thus it - // needs - // to run in the CREATED lifecycle so it can continue to watch while - // invisible - // See (b/291031862) for details - launch { - visibilityState.collect { visibilityState -> - // for b/296864006, we can not hide all the child views if - // visibilityState is STATE_HIDDEN. Because hiding all child views - // would cause the - // getWidth() of this view return 0, and that would cause the - // translation - // calculation fails in StatusIconContainer. Therefore, like class - // MobileIconBinder, instead of set the child views visibility to - // View.GONE, - // we set their visibility to View.INVISIBLE to make them invisible - // but - // keep the width. - ModernStatusBarViewVisibilityHelper.setVisibilityState( - visibilityState, - view.iconView, - view.dotView, - ) - } - } - - launch { - iconTint.collect { tint -> - val tintList = ColorStateList.valueOf(tint) - view.iconView.imageTintList = tintList - view.dotView.setDecorColor(tint) - } - } - - launch { - decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) } - } - - try { - awaitCancellation() - } finally { - isCollecting = false - } - } - } - } - - return object : SingleBindableStatusBarIconViewBinding { - override val decorTint: Int - get() = decorTint.value - - override val iconTint: Int - get() = iconTint.value - - override fun getShouldIconBeVisible(): Boolean { - return shouldBeVisible() - } - - override fun onVisibilityStateChanged(state: Int) { - visibilityState.value = state - } - - override fun onIconTintChanged(newTint: Int, contrastTint: Int) { - iconTint.value = newTint - } - - override fun onDecorTintChanged(newTint: Int) { - decorTint.value = newTint - } - - override fun isCollecting(): Boolean { - return isCollecting - } - } - } - } -} - -@VisibleForTesting -interface SingleBindableStatusBarIconViewBinding : ModernStatusBarViewBinding { - val iconTint: Int - val decorTint: Int -} 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 e9711284cfca..1528c9befda7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -26,6 +26,9 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.os.Handler; import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.MetricsLogger; @@ -34,7 +37,6 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.EventLogTags; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; -import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; import com.android.systemui.util.ListenerSet; @@ -43,14 +45,14 @@ import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; +import java.util.stream.Stream; /** * A manager which handles heads up notifications which is a special mode where * they simply peek from the top of the screen. */ -public abstract class BaseHeadsUpManager extends AlertingNotificationManager implements - HeadsUpManager { - private static final String TAG = "HeadsUpManager"; +public abstract class BaseHeadsUpManager implements HeadsUpManager { + private static final String TAG = "BaseHeadsUpManager"; private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>(); @@ -67,6 +69,14 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp private final UiEventLogger mUiEventLogger; + protected final SystemClock mSystemClock; + protected final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); + protected final HeadsUpManagerLogger mLogger; + protected int mMinimumDisplayTime; + protected int mStickyForSomeTimeAutoDismissTime; + protected int mAutoDismissTime; + protected DelayableExecutor mExecutor; + /** * Enum entry for notification peek logged from this class. */ @@ -91,7 +101,9 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger) { - super(logger, systemClock, executor); + mLogger = logger; + mExecutor = executor; + mSystemClock = systemClock; mContext = context; mAccessibilityMgr = accessibilityManagerWrapper; mUiEventLogger = uiEventLogger; @@ -138,20 +150,136 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp mListeners.remove(listener); } - /** Updates the notification with the given key. */ - public void updateNotification(@NonNull String key, boolean alert) { - super.updateNotification(key, alert); - HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); - if (alert && headsUpEntry != null) { - setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry)); + /** + * Called when posting a new notification that should appear on screen. + * Adds the notification to be managed. + * @param entry entry to show + */ + @Override + public void showNotification(@NonNull NotificationEntry entry) { + mLogger.logShowNotification(entry); + addEntry(entry); + updateNotification(entry.getKey(), true /* show */); + entry.setInterruption(); + } + + /** + * Try to remove the notification. May not succeed if the notification has not been shown long + * enough and needs to be kept around. + * @param key the key of the notification to remove + * @param releaseImmediately force a remove regardless of earliest removal time + * @return true if notification is removed, false otherwise + */ + @Override + public boolean removeNotification(@NonNull String key, boolean releaseImmediately) { + mLogger.logRemoveNotification(key, releaseImmediately); + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + if (headsUpEntry == null) { + return true; + } + if (releaseImmediately || canRemoveImmediately(key)) { + removeEntry(key); + } else { + headsUpEntry.removeAsSoonAsPossible(); + return false; + } + return true; + } + + + /** + * Called when the notification state has been updated. + * @param key the key of the entry that was updated + * @param show whether the notification should show again and force reevaluation of + * removal time + */ + public void updateNotification(@NonNull String key, boolean show) { + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + mLogger.logUpdateNotification(key, show, headsUpEntry != null); + if (headsUpEntry == null) { + // the entry was released before this update (i.e by a listener) This can happen + // with the groupmanager + return; + } + + headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + + if (show) { + headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification"); + if (headsUpEntry != null) { + setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry)); + } + } + } + + /** + * Clears all managed notifications. + */ + public void releaseAllImmediately() { + mLogger.logReleaseAllImmediately(); + // A copy is necessary here as we are changing the underlying map. This would cause + // undefined behavior if we iterated over the key set directly. + ArraySet<String> keysToRemove = new ArraySet<>(mHeadsUpEntryMap.keySet()); + for (String key : keysToRemove) { + removeEntry(key); + } + } + + /** + * Returns the entry if it is managed by this manager. + * @param key key of notification + * @return the entry + */ + @Nullable + public NotificationEntry getEntry(@NonNull String key) { + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + return headsUpEntry != null ? headsUpEntry.mEntry : null; + } + + /** + * Returns the stream of all current notifications managed by this manager. + * @return all entries + */ + @NonNull + @Override + public Stream<NotificationEntry> getAllEntries() { + return mHeadsUpEntryMap.values().stream().map(headsUpEntry -> headsUpEntry.mEntry); + } + + /** + * Whether or not there are any active notifications. + * @return true if there is an entry, false otherwise + */ + @Override + public boolean hasNotifications() { + return !mHeadsUpEntryMap.isEmpty(); + } + + /** + * @return true if the notification is managed by this manager + */ + public boolean isHeadsUpEntry(@NonNull String key) { + return mHeadsUpEntryMap.containsKey(key); + } + + /** + * @param key + * @return When a HUN entry should be removed in milliseconds from now + */ + @Override + public long getEarliestRemovalTime(String key) { + HeadsUpEntry entry = mHeadsUpEntryMap.get(key); + if (entry != null) { + return Math.max(0, entry.mEarliestRemovalTime - mSystemClock.elapsedRealtime()); } + return 0; } protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) { final HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); if (headsUpEntry == null) { // This should not happen since shouldHeadsUpBecomePinned is always called after adding - // the NotificationEntry into AlertingNotificationManager's mAlertEntries map. + // the NotificationEntry into mHeadsUpEntryMap. return hasFullScreenIntent(entry); } return hasFullScreenIntent(entry) && !headsUpEntry.mWasUnpinned; @@ -190,24 +318,65 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp return FLAG_CONTENT_VIEW_HEADS_UP; } - @Override - protected void onAlertEntryAdded(AlertEntry alertEntry) { - NotificationEntry entry = alertEntry.mEntry; + /** + * Add a new entry and begin managing it. + * @param entry the entry to add + */ + protected final void addEntry(@NonNull NotificationEntry entry) { + HeadsUpEntry headsUpEntry = createHeadsUpEntry(); + headsUpEntry.setEntry(entry); + mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); + onEntryAdded(headsUpEntry); + entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + entry.setIsHeadsUpEntry(true); + } + + /** + * Manager-specific logic that should occur when an entry is added. + * @param headsUpEntry entry added + */ + protected void onEntryAdded(HeadsUpEntry headsUpEntry) { + NotificationEntry entry = headsUpEntry.mEntry; entry.setHeadsUp(true); final boolean shouldPin = shouldHeadsUpBecomePinned(entry); - setEntryPinned((HeadsUpEntry) alertEntry, shouldPin); + setEntryPinned(headsUpEntry, shouldPin); EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */); for (OnHeadsUpChangedListener listener : mListeners) { listener.onHeadsUpStateChanged(entry, true); } } - @Override - protected void onAlertEntryRemoved(AlertEntry alertEntry) { - NotificationEntry entry = alertEntry.mEntry; + /** + * Remove a notification and reset the entry. + * @param key key of notification to remove + */ + protected final void removeEntry(@NonNull String key) { + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + if (headsUpEntry == null) { + return; + } + NotificationEntry entry = headsUpEntry.mEntry; + + // If the notification is animating, we will remove it at the end of the animation. + if (entry != null && entry.isExpandAnimationRunning()) { + return; + } + entry.demoteStickyHun(); + mHeadsUpEntryMap.remove(key); + onEntryRemoved(headsUpEntry); + entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + headsUpEntry.reset(); + } + + /** + * Manager-specific logic that should occur when an entry is removed. + * @param headsUpEntry entry removed + */ + protected void onEntryRemoved(HeadsUpEntry headsUpEntry) { + NotificationEntry entry = headsUpEntry.mEntry; entry.setHeadsUp(false); - setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */); + setEntryPinned(headsUpEntry, false /* isPinned */); EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); mLogger.logNotificationActuallyRemoved(entry); for (OnHeadsUpChangedListener listener : mListeners) { @@ -251,8 +420,8 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp * Snoozes all current Heads Up Notifications. */ public void snooze() { - for (String key : mAlertEntries.keySet()) { - AlertEntry entry = getHeadsUpEntry(key); + for (String key : mHeadsUpEntryMap.keySet()) { + HeadsUpEntry entry = getHeadsUpEntry(key); String packageName = entry.mEntry.getSbn().getPackageName(); String snoozeKey = snoozeKey(packageName, mUser); mLogger.logPackageSnoozed(snoozeKey); @@ -267,7 +436,7 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp @Nullable protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { - return (HeadsUpEntry) mAlertEntries.get(key); + return (HeadsUpEntry) mHeadsUpEntryMap.get(key); } /** @@ -281,11 +450,11 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp @Nullable protected HeadsUpEntry getTopHeadsUpEntry() { - if (mAlertEntries.isEmpty()) { + if (mHeadsUpEntryMap.isEmpty()) { return null; } HeadsUpEntry topEntry = null; - for (AlertEntry entry: mAlertEntries.values()) { + for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { if (topEntry == null || entry.compareTo(topEntry) < 0) { topEntry = (HeadsUpEntry) entry; } @@ -316,7 +485,7 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); pw.print(" now="); pw.println(mSystemClock.elapsedRealtime()); pw.print(" mUser="); pw.println(mUser); - for (AlertEntry entry: mAlertEntries.values()) { + for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { pw.print(" HeadsUpEntry="); pw.println(entry.mEntry); } int n = mSnoozedPackages.size(); @@ -335,8 +504,8 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp } private boolean hasPinnedNotificationInternal() { - for (String key : mAlertEntries.keySet()) { - AlertEntry entry = getHeadsUpEntry(key); + for (String key : mHeadsUpEntryMap.keySet()) { + HeadsUpEntry entry = getHeadsUpEntry(key); if (entry.mEntry.isRowPinned()) { return true; } @@ -349,7 +518,7 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp * @param userUnPinned The unpinned action is trigger by user real operation. */ public void unpinAll(boolean userUnPinned) { - for (String key : mAlertEntries.keySet()) { + for (String key : mHeadsUpEntryMap.keySet()) { HeadsUpEntry entry = getHeadsUpEntry(key); setEntryPinned(entry, false /* isPinned */); // maybe it got un sticky @@ -384,8 +553,8 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp if (a == null || b == null) { return Boolean.compare(a == null, b == null); } - AlertEntry aEntry = getHeadsUpEntry(a.getKey()); - AlertEntry bEntry = getHeadsUpEntry(b.getKey()); + HeadsUpEntry aEntry = getHeadsUpEntry(a.getKey()); + HeadsUpEntry bEntry = getHeadsUpEntry(b.getKey()); if (aEntry == null || bEntry == null) { return Boolean.compare(aEntry == null, bEntry == null); } @@ -419,18 +588,37 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp } } + /** + * Whether or not the entry can be removed currently. If it hasn't been on screen long enough + * it should not be removed unless forced + * @param key the key to check if removable + * @return true if the entry can be removed + */ @Override public boolean canRemoveImmediately(@NonNull String key) { HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); if (headsUpEntry != null && headsUpEntry.mUserActionMayIndirectlyRemove) { return true; } - return super.canRemoveImmediately(key); + return headsUpEntry == null || headsUpEntry.wasShownLongEnough() + || headsUpEntry.mEntry.isRowDismissed(); } - @NonNull + /** + * @param key + * @return true if the entry is (pinned and expanded) or (has an active remote input) + */ @Override - protected HeadsUpEntry createAlertEntry() { + public boolean isSticky(String key) { + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + if (headsUpEntry != null) { + return headsUpEntry.isSticky(); + } + return false; + } + + @NonNull + protected HeadsUpEntry createHeadsUpEntry() { return new HeadsUpEntry(); } @@ -451,28 +639,82 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp * This represents a notification and how long it is in a heads up mode. It also manages its * lifecycle automatically when created. */ - protected class HeadsUpEntry extends AlertEntry { + protected class HeadsUpEntry implements Comparable<HeadsUpEntry> { public boolean mRemoteInputActive; public boolean mUserActionMayIndirectlyRemove; protected boolean mExpanded; protected boolean mWasUnpinned; - @Override + @Nullable public NotificationEntry mEntry; + public long mPostTime; + public long mEarliestRemovalTime; + + @Nullable protected Runnable mRemoveRunnable; + + @Nullable private Runnable mCancelRemoveRunnable; + + public void setEntry(@NonNull final NotificationEntry entry) { + setEntry(entry, () -> removeEntry(entry.getKey())); + } + + public void setEntry(@NonNull final NotificationEntry entry, + @Nullable Runnable removeRunnable) { + mEntry = entry; + mRemoveRunnable = removeRunnable; + + mPostTime = calculatePostTime(); + updateEntry(true /* updatePostTime */, "setEntry"); + } + + /** + * Updates an entry's removal time. + * @param updatePostTime whether or not to refresh the post time + */ + public void updateEntry(boolean updatePostTime, @Nullable String reason) { + mLogger.logUpdateEntry(mEntry, updatePostTime, reason); + + final long now = mSystemClock.elapsedRealtime(); + mEarliestRemovalTime = now + mMinimumDisplayTime; + + if (updatePostTime) { + mPostTime = Math.max(mPostTime, now); + } + + if (isSticky()) { + removeAutoRemovalCallbacks("updateEntry (sticky)"); + return; + } + + final long finishTime = calculateFinishTime(); + final long timeLeft = Math.max(finishTime - now, mMinimumDisplayTime); + scheduleAutoRemovalCallback(timeLeft, "updateEntry (not sticky)"); + } + + /** + * Whether or not the notification is "sticky" i.e. should stay on screen regardless + * of the timer (forever) and should be removed externally. + * @return true if the notification is sticky + */ public boolean isSticky() { return (mEntry.isRowPinned() && mExpanded) || mRemoteInputActive || hasFullScreenIntent(mEntry); } - @Override public boolean isStickyForSomeTime() { return mEntry.isStickyAndNotDemoted(); } - @Override - public int compareTo(@NonNull AlertEntry alertEntry) { - HeadsUpEntry headsUpEntry = (HeadsUpEntry) alertEntry; + /** + * Whether the notification has been on screen long enough and can be removed. + * @return true if the notification has been on screen long enough + */ + public boolean wasShownLongEnough() { + return mEarliestRemovalTime < mSystemClock.elapsedRealtime(); + } + + public int compareTo(@NonNull HeadsUpEntry headsUpEntry) { boolean isPinned = mEntry.isRowPinned(); boolean otherPinned = headsUpEntry.mEntry.isRowPinned(); if (isPinned && !otherPinned) { @@ -503,31 +745,90 @@ public abstract class BaseHeadsUpManager extends AlertingNotificationManager imp return 1; } - return super.compareTo(headsUpEntry); + if (mPostTime > headsUpEntry.mPostTime) { + return -1; + } else if (mPostTime == headsUpEntry.mPostTime) { + return mEntry.getKey().compareTo(headsUpEntry.mEntry.getKey()); + } else { + return 1; + } } public void setExpanded(boolean expanded) { this.mExpanded = expanded; } - @Override public void reset() { - super.reset(); + removeAutoRemovalCallbacks("reset()"); + mEntry = null; + mRemoveRunnable = null; mExpanded = false; mRemoteInputActive = false; } - @Override + /** + * Clear any pending removal runnables. + */ + public void removeAutoRemovalCallbacks(@Nullable String reason) { + final boolean removed = removeAutoRemovalCallbackInternal(); + + if (removed) { + mLogger.logAutoRemoveCanceled(mEntry, reason); + } + } + + public void scheduleAutoRemovalCallback(long delayMillis, @NonNull String reason) { + if (mRemoveRunnable == null) { + Log.wtf(TAG, "scheduleAutoRemovalCallback with no callback set"); + return; + } + + final boolean removed = removeAutoRemovalCallbackInternal(); + + if (removed) { + mLogger.logAutoRemoveRescheduled(mEntry, delayMillis, reason); + } else { + mLogger.logAutoRemoveScheduled(mEntry, delayMillis, reason); + } + + mCancelRemoveRunnable = mExecutor.executeDelayed(mRemoveRunnable, + delayMillis); + } + + public boolean removeAutoRemovalCallbackInternal() { + final boolean scheduled = (mCancelRemoveRunnable != null); + + if (scheduled) { + mCancelRemoveRunnable.run(); + mCancelRemoveRunnable = null; + } + + return scheduled; + } + + /** + * Remove the entry at the earliest allowed removal time. + */ + public void removeAsSoonAsPossible() { + if (mRemoveRunnable != null) { + final long timeLeft = mEarliestRemovalTime - mSystemClock.elapsedRealtime(); + scheduleAutoRemovalCallback(timeLeft, "removeAsSoonAsPossible"); + } + } + + /** + * Calculate what the post time of a notification is at some current time. + * @return the post time + */ protected long calculatePostTime() { // The actual post time will be just after the heads-up really slided in - return super.calculatePostTime() + mTouchAcceptanceDelay; + return mSystemClock.elapsedRealtime() + mTouchAcceptanceDelay; } /** * @return When the notification should auto-dismiss itself, based on * {@link SystemClock#elapsedRealtime()} */ - @Override protected long calculateFinishTime() { final long duration = getRecommendedHeadsUpTimeoutMs( isStickyForSomeTime() ? mStickyForSomeTimeAutoDismissTime : mAutoDismissTime); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt index d9527fe98b1a..b8c7e202ce7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt @@ -71,7 +71,7 @@ interface HeadsUpManager : Dumpable { fun hasPinnedHeadsUp(): Boolean /** Returns whether or not the given notification is alerting and managed by this manager. */ - fun isAlerting(key: String): Boolean + fun isHeadsUpEntry(key: String): Boolean fun isHeadsUpGoingAway(): Boolean @@ -213,7 +213,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager { override fun getTouchableRegion(): Region? = null override fun getTopEntry() = null override fun hasPinnedHeadsUp() = false - override fun isAlerting(key: String) = false + override fun isHeadsUpEntry(key: String) = false override fun isHeadsUpGoingAway() = false override fun isSnoozed(packageName: String) = false override fun isSticky(key: String?) = false diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java index d8ef981b6a56..da6bfe84eee7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -48,10 +48,10 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -76,7 +76,7 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { protected MockitoSession mStaticMockSession; - protected final SceneTestUtils mSceneTestUtils = new SceneTestUtils(this); + protected final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); protected @Mock DeviceEntryInteractor mDeviceEntryInteractor; protected @Mock LockIconView mLockIconView; protected @Mock AnimatedStateListDrawable mIconDrawable; @@ -175,7 +175,7 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { mPrimaryBouncerInteractor, mContext, () -> mDeviceEntryInteractor, - mSceneTestUtils.getFakeSceneContainerFlags() + mKosmos.getFakeSceneContainerFlags() ); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java index 132bdb55180a..b0887efed4d7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java @@ -373,7 +373,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { @Test public void longPress_showBouncer_sceneContainerNotEnabled() { init(/* useMigrationFlag= */ false); - mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(false); + mKosmos.getFakeSceneContainerFlags().setEnabled(false); when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false); // WHEN longPress @@ -387,7 +387,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { @Test public void longPress_showBouncer() { init(/* useMigrationFlag= */ false); - mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(true); + mKosmos.getFakeSceneContainerFlags().setEnabled(true); when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false); // WHEN longPress @@ -401,7 +401,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { @Test public void longPress_falsingTriggered_doesNotShowBouncer() { init(/* useMigrationFlag= */ false); - mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(true); + mKosmos.getFakeSceneContainerFlags().setEnabled(true); when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(true); // WHEN longPress diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 2afb3a15b4be..d86d12303140 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -640,11 +640,10 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Test public void deleteWindowMagnification_enabling_expectedValuesAndInvokeCallback() throws RemoteException { - enableWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration, mAnimationCallback); - Mockito.reset(mSpyController); + resetMockObjects(); getInstrumentation().runOnMainSync(() -> { mWindowMagnificationAnimationController.deleteWindowMagnification( mAnimationCallback2); @@ -658,6 +657,11 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mValueAnimator.end(); }); + // wait for animation returns + waitForIdleSync(); + + // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} will only + // be triggered once in {@link ValueAnimator#end()} verify(mSpyController).enableWindowMagnificationInternal( mScaleCaptor.capture(), mCenterXCaptor.capture(), mCenterYCaptor.capture(), @@ -717,7 +721,11 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { deleteWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration, mAnimationCallback); - deleteWindowMagnificationAndWaitAnimating(0, null); + // Verifying that WindowMagnificationController#deleteWindowMagnification is never called + // in previous steps + verify(mSpyController, never()).deleteWindowMagnification(); + + deleteWindowMagnificationWithoutAnimation(); verify(mSpyController).deleteWindowMagnification(); verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN); @@ -810,6 +818,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification( targetScale, targetCenterX, targetCenterY, null); }); + // wait for animation returns + waitForIdleSync(); } private void enableWindowMagnificationAndWaitAnimating(long duration, @@ -829,12 +839,16 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { targetScale, targetCenterX, targetCenterY, callback); advanceTimeBy(duration); }); + // wait for animation returns + waitForIdleSync(); } private void deleteWindowMagnificationWithoutAnimation() { getInstrumentation().runOnMainSync(() -> { mWindowMagnificationAnimationController.deleteWindowMagnification(null); }); + // wait for animation returns + waitForIdleSync(); } private void deleteWindowMagnificationAndWaitAnimating(long duration, @@ -843,6 +857,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.deleteWindowMagnification(callback); advanceTimeBy(duration); }); + // wait for animation returns + waitForIdleSync(); } private void verifyStartValue(ArgumentCaptor<Float> captor, float startValue) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 68879a54cfe4..5e5273b779c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -55,8 +55,6 @@ import android.os.Build; import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -120,10 +118,6 @@ public class MenuViewLayerTest extends SysuiTestCase { @Rule public MockitoRule mockito = MockitoJUnit.rule(); - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Spy private SysuiTestableContext mSpyContext = getContext(); @Mock diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index 7c626a141a4a..e0c6bbad5635 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -44,6 +44,8 @@ import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.util.concurrency.FakeExecutor @@ -56,6 +58,7 @@ import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import org.junit.Before @@ -89,6 +92,9 @@ class BackActionInteractorTest : SysuiTestCase() { @Mock private lateinit var onBackInvokedDispatcher: WindowOnBackInvokedDispatcher @Mock private lateinit var iStatusBarService: IStatusBarService @Mock private lateinit var headsUpManager: HeadsUpManager + private val activeNotificationsRepository = ActiveNotificationListRepository() + private val activeNotificationsInteractor = + ActiveNotificationsInteractor(activeNotificationsRepository, StandardTestDispatcher()) private val keyguardRepository = FakeKeyguardRepository() private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { @@ -98,6 +104,7 @@ class BackActionInteractorTest : SysuiTestCase() { keyguardRepository, headsUpManager, powerInteractor, + activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 0ee09390d03a..43f7c60721ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics import android.app.admin.DevicePolicyManager +import android.content.pm.PackageManager import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager @@ -79,6 +80,8 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +private const val OP_PACKAGE_NAME = "biometric.testapp" + @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -109,6 +112,8 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var authController: AuthController @Mock lateinit var selectedUserInteractor: SelectedUserInteractor + @Mock + private lateinit var packageManager: PackageManager private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) @@ -134,6 +139,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) + private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) private var authContainer: TestAuthContainerView? = null @@ -156,6 +162,9 @@ open class AuthContainerViewTest : SysuiTestCase() { selectedUserInteractor, testScope.backgroundScope, ) + // Set up default logo icon + whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + context.setMockPackageManager(packageManager) } @After @@ -533,6 +542,7 @@ open class AuthContainerViewTest : SysuiTestCase() { mPromptInfo = PromptInfo().apply { this.authenticators = authenticators } + mOpPackageName = OP_PACKAGE_NAME }, testScope.backgroundScope, fingerprintProps, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index ec7ce634fd78..b39e09df9d2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -43,6 +43,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 9 private const val CHALLENGE = 90L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -102,7 +103,8 @@ class PromptRepositoryImplTest : SysuiTestCase() { PromptInfo().apply { isConfirmationRequested = case }, USER_ID, CHALLENGE, - PromptKind.Biometric() + PromptKind.Biometric(), + OP_PACKAGE_NAME ) assertThat(isConfirmationRequired).isEqualTo(case) @@ -120,7 +122,8 @@ class PromptRepositoryImplTest : SysuiTestCase() { PromptInfo().apply { isConfirmationRequested = case }, USER_ID, CHALLENGE, - PromptKind.Biometric() + PromptKind.Biometric(), + OP_PACKAGE_NAME ) assertThat(isConfirmationRequired).isTrue() @@ -133,17 +136,19 @@ class PromptRepositoryImplTest : SysuiTestCase() { val kind = PromptKind.Pin val promptInfo = PromptInfo() - repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind) + repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind, OP_PACKAGE_NAME) assertThat(repository.kind.value).isEqualTo(kind) assertThat(repository.userId.value).isEqualTo(USER_ID) assertThat(repository.challenge.value).isEqualTo(CHALLENGE) assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + assertThat(repository.opPackageName.value).isEqualTo(OP_PACKAGE_NAME) repository.unsetPrompt() assertThat(repository.promptInfo.value).isNull() assertThat(repository.userId.value).isNull() assertThat(repository.challenge.value).isNull() + assertThat(repository.opPackageName.value).isNull() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt index dcefea28d4c8..8a46c0c6da9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -30,6 +30,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 22 private const val OPERATION_ID = 100L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -114,7 +115,8 @@ class PromptCredentialInteractorTest : SysuiTestCase() { }, kind = kind, userId = USER_ID, - challenge = OPERATION_ID + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME ) assertThat(prompt?.title).isEqualTo(title) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index f15b738f3e95..52b42750847a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -51,6 +51,7 @@ private const val NEGATIVE_TEXT = "escape" private const val USER_ID = 8 private const val CHALLENGE = 999L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -113,13 +114,20 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { assertThat(currentPrompt).isNull() - interactor.useBiometricsForAuthentication(info, USER_ID, CHALLENGE, modalities) + interactor.useBiometricsForAuthentication( + info, + USER_ID, + CHALLENGE, + modalities, + OP_PACKAGE_NAME + ) assertThat(currentPrompt).isNotNull() assertThat(currentPrompt?.title).isEqualTo(TITLE) assertThat(currentPrompt?.description).isEqualTo(DESCRIPTION) assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE) assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT) + assertThat(currentPrompt?.opPackageName).isEqualTo(OP_PACKAGE_NAME) if (allowCredentialFallback) { assertThat(credentialKind).isSameInstanceAs(PromptKind.Password) @@ -167,7 +175,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { assertThat(currentPrompt).isNull() - interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE) + interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE, OP_PACKAGE_NAME) // not using biometrics, should be null with no fallback option assertThat(currentPrompt).isNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index bd4973d65006..a46167a423f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -1,6 +1,7 @@ package com.android.systemui.biometrics.domain.model -import android.hardware.biometrics.PromptContentListItemBulletedText +import android.graphics.Bitmap +import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -8,6 +9,7 @@ import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.promptInfo import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricUserInfo +import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -15,6 +17,7 @@ import org.junit.runners.JUnit4 private const val USER_ID = 2 private const val OPERATION_ID = 8L +private const val OP_PACKAGE_NAME = "biometric.testapp" @SmallTest @RunWith(JUnit4::class) @@ -22,19 +25,22 @@ class BiometricPromptRequestTest : SysuiTestCase() { @Test fun biometricRequestFromPromptInfo() { + val logoRes = R.drawable.ic_cake val title = "what" val subtitle = "a" val description = "request" val contentView = PromptVerticalListContentView.Builder() .setDescription("content description") - .addListItem(PromptContentListItemBulletedText("content text")) + .addListItem(PromptContentItemBulletedText("content item 1")) + .addListItem(PromptContentItemBulletedText("content item 2"), 1) .build() val fpPros = fingerprintSensorPropertiesInternal().first() val request = BiometricPromptRequest.Biometric( promptInfo( + logoRes = logoRes, title = title, subtitle = subtitle, description = description, @@ -43,8 +49,10 @@ class BiometricPromptRequestTest : SysuiTestCase() { BiometricUserInfo(USER_ID), BiometricOperationInfo(OPERATION_ID), BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, ) + assertThat(request.logoRes).isEqualTo(logoRes) assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) @@ -56,6 +64,23 @@ class BiometricPromptRequestTest : SysuiTestCase() { } @Test + fun biometricRequestLogoBitmapFromPromptInfo() { + val logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888) + val fpPros = fingerprintSensorPropertiesInternal().first() + val request = + BiometricPromptRequest.Biometric( + promptInfo( + logoBitmap = logoBitmap, + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, + ) + assertThat(request.logoBitmap).isEqualTo(logoBitmap) + } + + @Test fun credentialRequestFromPromptInfo() { val title = "what" val subtitle = "a" 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 bf61c2e6d32c..3888f2b940b3 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 @@ -16,9 +16,12 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Bitmap import android.graphics.Point -import android.hardware.biometrics.PromptContentListItemBulletedText +import android.graphics.drawable.BitmapDrawable +import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView @@ -76,6 +79,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 4 private const val CHALLENGE = 2L private const val DELAY = 1000L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -88,9 +92,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Mock private lateinit var authController: AuthController @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor @Mock private lateinit var udfpsUtils: UdfpsUtils + @Mock private lateinit var packageManager: PackageManager private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val testScope = TestScope() + private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) + private val logoResFromApp = R.drawable.ic_cake + private val logoFromApp = context.getDrawable(logoResFromApp) + private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565) private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository private lateinit var promptRepository: FakePromptRepository @@ -140,7 +149,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa selector.resetPrompt() promptContentView = PromptVerticalListContentView.Builder() - .addListItem(PromptContentListItemBulletedText("test")) + .addListItem(PromptContentItemBulletedText("content item 1")) + .addListItem(PromptContentItemBulletedText("content item 2"), 1) .build() viewModel = @@ -152,6 +162,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa udfpsUtils ) iconViewModel = viewModel.iconViewModel + + // Set up default logo icon and app customized icon + whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + context.setMockPackageManager(packageManager) + val resources = context.getOrCreateTestableResources() + resources.addOverride(logoResFromApp, logoFromApp) } @Test @@ -1226,6 +1242,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(contentView).isNull() } + @Test + fun defaultLogoIfNoLogoSet() = runGenericTest { + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isEqualTo(defaultLogoIcon) + } + + @Test + fun logoResSetByApp() = + runGenericTest(logoRes = logoResFromApp) { + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isEqualTo(logoFromApp) + } + + @Test + fun logoBitmapSetByApp() = + runGenericTest(logoBitmap = logoBitmapFromApp) { + val logo by collectLastValue(viewModel.logo) + assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp) + } + /** Asserts that the selected buttons are visible now. */ private suspend fun TestScope.assertButtonsVisible( tryAgain: Boolean = false, @@ -1247,6 +1283,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa allowCredentialFallback: Boolean = false, description: String? = null, contentView: PromptContentView? = null, + logoRes: Int = -1, + logoBitmap: Bitmap? = null, block: suspend TestScope.() -> Unit ) { selector.initializePrompt( @@ -1256,6 +1294,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa face = testCase.face, descriptionFromApp = description, contentViewFromApp = contentView, + logoResFromApp = logoRes, + logoBitmapFromApp = logoBitmap, ) // put the view model in the initial authenticating state, unless explicitly skipped @@ -1433,9 +1473,13 @@ private fun PromptSelectorInteractor.initializePrompt( allowCredentialFallback: Boolean = false, descriptionFromApp: String? = null, contentViewFromApp: PromptContentView? = null, + logoResFromApp: Int = -1, + logoBitmapFromApp: Bitmap? = null, ) { val info = PromptInfo().apply { + logoRes = logoResFromApp + logoBitmap = logoBitmapFromApp title = "t" subtitle = "s" description = descriptionFromApp @@ -1444,11 +1488,13 @@ private fun PromptSelectorInteractor.initializePrompt( isDeviceCredentialAllowed = allowCredentialFallback isConfirmationRequested = requireConfirmation } + useBiometricsForAuthentication( info, USER_ID, CHALLENGE, BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), + OP_PACKAGE_NAME, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt index 44c57f34fa1b..134c40da1033 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt @@ -3,8 +3,9 @@ package com.android.systemui.bouncer.data.repository 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.log.table.TableLogBuffer -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.SystemClock @@ -23,8 +24,8 @@ class KeyguardBouncerRepositoryTest : SysuiTestCase() { @Mock private lateinit var systemClock: SystemClock @Mock private lateinit var bouncerLogger: TableLogBuffer - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope lateinit var underTest: KeyguardBouncerRepository diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt deleted file mode 100644 index 30a5497d0a14..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.communal.data.repository - -import android.content.pm.UserInfo -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.coroutines.collectLastValue -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.core.FakeLogBuffer -import com.android.systemui.settings.FakeUserTracker -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.util.settings.FakeSettings -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalTutorialRepositoryImplTest : SysuiTestCase() { - private lateinit var secureSettings: FakeSettings - private lateinit var userRepository: FakeUserRepository - private lateinit var userTracker: FakeUserTracker - private lateinit var logBuffer: LogBuffer - - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - logBuffer = FakeLogBuffer.Factory.create() - secureSettings = FakeSettings() - userRepository = FakeUserRepository() - val listOfUserInfo = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(listOfUserInfo) - - userTracker = FakeUserTracker() - userTracker.set( - userInfos = listOfUserInfo, - selectedUserIndex = 0, - ) - } - - @Test - fun tutorialSettingState_defaultToNotStarted() = - testScope.runTest { - val repository = initCommunalTutorialRepository() - val tutorialSettingState = collectLastValue(repository.tutorialSettingState)() - assertThat(tutorialSettingState) - .isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED) - } - - @Test - fun tutorialSettingState_whenTutorialSettingsUpdatedToStarted() = - testScope.runTest { - val repository = initCommunalTutorialRepository() - setTutorialStateSetting(Settings.Secure.HUB_MODE_TUTORIAL_STARTED) - val tutorialSettingState = collectLastValue(repository.tutorialSettingState)() - assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_STARTED) - } - - @Test - fun tutorialSettingState_whenTutorialSettingsUpdatedToCompleted() = - testScope.runTest { - val repository = initCommunalTutorialRepository() - setTutorialStateSetting(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - val tutorialSettingState = collectLastValue(repository.tutorialSettingState)() - assertThat(tutorialSettingState).isEqualTo(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - } - - private fun initCommunalTutorialRepository(): CommunalTutorialRepositoryImpl { - return CommunalTutorialRepositoryImpl( - testScope.backgroundScope, - testDispatcher, - userRepository, - secureSettings, - userTracker, - logBuffer - ) - } - - private fun setTutorialStateSetting( - @Settings.Secure.HubModeTutorialState state: Int, - user: UserInfo = MAIN_USER_INFO - ) { - secureSettings.putIntForUser(Settings.Secure.HUB_MODE_TUTORIAL_STATE, state, user.id) - } - - companion object { - private val MAIN_USER_INFO = - UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt new file mode 100644 index 000000000000..d397fc202637 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -0,0 +1,211 @@ +/* + * 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.stickykeys.ui.viewmodel + +import android.hardware.input.InputManager +import android.hardware.input.StickyModifierState +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +@SmallTest +@RunWith(JUnit4::class) +class StickyKeysIndicatorViewModelTest : SysuiTestCase() { + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private lateinit var viewModel: StickyKeysIndicatorViewModel + private val inputManager = mock<InputManager>() + private val keyboardRepository = FakeKeyboardRepository() + private val captor = + ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) + + @Before + fun setup() { + val stickyKeysRepository = StickyKeysRepositoryImpl( + inputManager, + dispatcher, + mock<StickyKeysLogger>() + ) + viewModel = + StickyKeysIndicatorViewModel( + stickyKeysRepository = stickyKeysRepository, + keyboardRepository = keyboardRepository, + applicationScope = testScope.backgroundScope, + ) + } + + @Test + fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnected() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + runCurrent() + verifyZeroInteractions(inputManager) + + keyboardRepository.setIsAnyKeyboardConnected(true) + runCurrent() + + verify(inputManager) + .registerStickyModifierStateListener( + any(), + any(InputManager.StickyModifierStateListener::class.java) + ) + } + } + + @Test + fun stopsListeningToStickyKeysWhenKeyboardDisconnects() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + runCurrent() + + keyboardRepository.setIsAnyKeyboardConnected(false) + runCurrent() + + verify(inputManager).unregisterStickyModifierStateListener(any()) + } + } + + @Test + fun emitsStickyKeysListWhenStickyKeyIsPressed() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf(ALT to false)) + + assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(false))) + } + } + + @Test + fun emitsEmptyListWhenNoStickyKeysAreActive() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(emptyMap()) + + assertThat(stickyKeys).isEqualTo(emptyMap<ModifierKey, Locked>()) + } + } + + @Test + fun passesAllStickyKeysToDialog() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + ALT to false, + META to false, + SHIFT to false)) + + assertThat(stickyKeys).isEqualTo(mapOf( + ALT to Locked(false), + META to Locked(false), + SHIFT to Locked(false), + )) + } + } + + @Test + fun showsOnlyLockedStateIfKeyIsStickyAndLocked() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + ALT to false, + ALT to true)) + + assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(true))) + } + } + + @Test + fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + META to false, + SHIFT to false, // shift is sticky but not locked + CTRL to false)) + val previousShiftIndex = stickyKeys?.toList()?.indexOf(SHIFT to Locked(false)) + + setStickyKeys(mapOf( + SHIFT to false, + SHIFT to true, // shift is now locked + META to false, + CTRL to false)) + assertThat(stickyKeys?.toList()?.indexOf(SHIFT to Locked(true))) + .isEqualTo(previousShiftIndex) + } + } + + private fun TestScope.setStickyKeys(keys: Map<ModifierKey, Boolean>) { + runCurrent() + verify(inputManager).registerStickyModifierStateListener(any(), captor.capture()) + captor.value.onStickyModifierStateChanged(TestStickyModifierState(keys)) + runCurrent() + } + + private class TestStickyModifierState(private val keys: Map<ModifierKey, Boolean>) : + StickyModifierState() { + + private fun isOn(key: ModifierKey) = keys.any { it.key == key && !it.value } + private fun isLocked(key: ModifierKey) = keys.any { it.key == key && it.value } + + override fun isAltGrModifierLocked() = isLocked(ALT_GR) + override fun isAltGrModifierOn() = isOn(ALT_GR) + override fun isAltModifierLocked() = isLocked(ALT) + override fun isAltModifierOn() = isOn(ALT) + override fun isCtrlModifierLocked() = isLocked(CTRL) + override fun isCtrlModifierOn() = isOn(CTRL) + override fun isMetaModifierLocked() = isLocked(META) + override fun isMetaModifierOn() = isOn(META) + override fun isShiftModifierLocked() = isLocked(SHIFT) + override fun isShiftModifierOn() = isOn(SHIFT) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 0ea4e9f8d416..8b6611f339c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -1342,14 +1342,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD) runCurrent() - // WHEN the keyguard is occluded and aod ends + // WHEN the keyguard is occluded keyguardRepository.setKeyguardOccluded(true) - keyguardRepository.setDozeTransitionModel( - DozeTransitionModel( - from = DozeStateModel.DOZE_AOD, - to = DozeStateModel.FINISH, - ) - ) runCurrent() val info = @@ -1366,6 +1360,30 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun aodToPrimaryBouncer() = + testScope.runTest { + // GIVEN a prior transition has run to AOD + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD) + runCurrent() + + // WHEN the primary bouncer is set to show + bouncerRepository.setPrimaryShow(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to OCCLUDED should occur + assertThat(info.ownerName).isEqualTo("FromAodTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.AOD) + assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun lockscreenToOccluded_fromCameraGesture() = testScope.runTest { // GIVEN a prior transition has run to LOCKSCREEN diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index a4d217f1af79..5dd37ae46ee8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -21,13 +21,17 @@ import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.util.mockito.whenever import kotlin.test.Test +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.runner.RunWith import org.mockito.Mock @@ -48,30 +52,58 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { @Mock private lateinit var smallClockView: View @Mock private lateinit var smallClockFaceLayout: ClockFaceLayout @Mock private lateinit var largeClockFaceLayout: ClockFaceLayout + @Mock private lateinit var clockViewModel: KeyguardClockViewModel + private val clockSize = MutableStateFlow(LARGE) + private val currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null) @Before fun setup() { MockitoAnnotations.initMocks(this) + whenever(clockViewModel.clockSize).thenReturn(clockSize) + whenever(clockViewModel.currentClock).thenReturn(currentClock) + whenever(clockViewModel.burnInLayer).thenReturn(burnInLayer) + } + + @Test + fun addClockViews_WeatherClock() { + setupWeatherClock() + KeyguardClockViewBinder.addClockViews(clock, rootView) + verify(rootView).addView(smallClockView) + verify(rootView).addView(largeClockView) } @Test fun addClockViews_nonWeatherClock() { setupNonWeatherClock() - KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer) + KeyguardClockViewBinder.addClockViews(clock, rootView) verify(rootView).addView(smallClockView) verify(rootView).addView(largeClockView) - verify(burnInLayer).addView(smallClockView) + } + @Test + fun addClockViewsToBurnInLayer_LargeWeatherClock() { + setupWeatherClock() + clockSize.value = LARGE + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) + verify(burnInLayer).removeView(smallClockView) + verify(burnInLayer).addView(largeClockView) + } + + @Test + fun addClockViewsToBurnInLayer_LargeNonWeatherClock() { + setupNonWeatherClock() + clockSize.value = LARGE + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) + verify(burnInLayer).removeView(smallClockView) verify(burnInLayer, never()).addView(largeClockView) } @Test - fun addClockViews_WeatherClock() { - setupWeatherClock() - KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer) - verify(rootView).addView(smallClockView) - verify(rootView).addView(largeClockView) + fun addClockViewsToBurnInLayer_SmallClock() { + setupNonWeatherClock() + clockSize.value = SMALL + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) verify(burnInLayer).addView(smallClockView) - verify(burnInLayer).addView(largeClockView) + verify(burnInLayer).removeView(largeClockView) } private fun setupWeatherClock() { @@ -99,5 +131,7 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) + whenever(clockViewModel.clock).thenReturn(clock) + currentClock.value = clock } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 070a0ccd1232..57b555989166 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -22,6 +22,7 @@ import android.content.res.Resources import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.res.R @@ -31,6 +32,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import dagger.Lazy +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -46,6 +49,8 @@ class ClockSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> + private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true) private lateinit var underTest: ClockSection @@ -104,12 +109,15 @@ class ClockSectionTest : SysuiTestCase() { whenever(packageManager.getResourcesForApplication(anyString())).thenReturn(remoteResources) mContext.setMockPackageManager(packageManager) + whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) + underTest = ClockSection( keyguardClockInteractor, keyguardClockViewModel, mContext, splitShadeStateController, + blueprintInteractor ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt index 28da957d31b0..deb3a83fcbee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt @@ -26,6 +26,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R @@ -34,7 +36,8 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.StateFlow +import dagger.Lazy +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -50,7 +53,8 @@ class SmartspaceSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel @Mock private lateinit var lockscreenSmartspaceController: LockscreenSmartspaceController @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController - @Mock private lateinit var hasCustomWeatherDataDisplay: StateFlow<Boolean> + @Mock private lateinit var keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor + @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> private val smartspaceView = View(mContext).also { it.id = sharedR.id.bc_smartspace_view } private val weatherView = View(mContext).also { it.id = sharedR.id.weather_smartspace_view } @@ -58,17 +62,22 @@ class SmartspaceSectionTest : SysuiTestCase() { private lateinit var constraintLayout: ConstraintLayout private lateinit var constraintSet: ConstraintSet + private val clockShouldBeCentered = MutableStateFlow(false) + private val hasCustomWeatherDataDisplay = MutableStateFlow(false) + @Before fun setup() { MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) underTest = SmartspaceSection( + mContext, keyguardClockViewModel, keyguardSmartspaceViewModel, - mContext, + keyguardSmartspaceInteractor, lockscreenSmartspaceController, keyguardUnlockAnimationController, + blueprintInteractor ) constraintLayout = ConstraintLayout(mContext) whenever(lockscreenSmartspaceController.buildAndConnectView(any())) @@ -78,6 +87,7 @@ class SmartspaceSectionTest : SysuiTestCase() { whenever(lockscreenSmartspaceController.buildAndConnectDateView(any())).thenReturn(dateView) whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay) .thenReturn(hasCustomWeatherDataDisplay) + whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) constraintSet = ConstraintSet() } @@ -115,7 +125,7 @@ class SmartspaceSectionTest : SysuiTestCase() { fun testConstraintsWhenNotHasCustomWeatherDataDisplay() { whenever(keyguardSmartspaceViewModel.isSmartspaceEnabled).thenReturn(true) whenever(keyguardSmartspaceViewModel.isDateWeatherDecoupled).thenReturn(true) - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false) + hasCustomWeatherDataDisplay.value = false underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) assertWeatherSmartspaceConstrains(constraintSet) @@ -129,7 +139,7 @@ class SmartspaceSectionTest : SysuiTestCase() { @Test fun testConstraintsWhenHasCustomWeatherDataDisplay() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true) + hasCustomWeatherDataDisplay.value = true underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) assertWeatherSmartspaceConstrains(constraintSet) @@ -140,7 +150,7 @@ class SmartspaceSectionTest : SysuiTestCase() { @Test fun testNormalDateWeatherVisibility() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false) + hasCustomWeatherDataDisplay.value = false whenever(keyguardSmartspaceViewModel.isWeatherEnabled).thenReturn(true) underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) @@ -153,7 +163,7 @@ class SmartspaceSectionTest : SysuiTestCase() { } @Test fun testCustomDateWeatherVisibility() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true) + hasCustomWeatherDataDisplay.value = true underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 73885f4d428d..6be92756ba8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -104,18 +104,19 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var dreamOverlayCallback: ArgumentCaptor<(DreamOverlayStateController.Callback)> @JvmField @Rule val mockito = MockitoJUnit.rule() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() - private val communalRepository = FakeCommunalRepository(isCommunalEnabled = true) + private val communalRepository = + FakeCommunalRepository(applicationScope = testScope.backgroundScope) private val communalInteractor = CommunalInteractorFactory.create(communalRepository = communalRepository).communalInteractor private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) @Before fun setup() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt index 45f0a8c62125..44c411fdb1d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt @@ -66,6 +66,11 @@ class FakeMediaProjectionManager { private const val DEFAULT_PACKAGE_NAME = "com.media.projection.test" private val DEFAULT_USER_HANDLE = UserHandle.getUserHandleForUid(UserHandle.myUserId()) - private val DEFAULT_INFO = MediaProjectionInfo(DEFAULT_PACKAGE_NAME, DEFAULT_USER_HANDLE) + private val DEFAULT_INFO = + MediaProjectionInfo( + DEFAULT_PACKAGE_NAME, + DEFAULT_USER_HANDLE, + /* launchCookie = */ null + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt new file mode 100644 index 000000000000..60eb3aec190f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt @@ -0,0 +1,107 @@ +/* + * 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.qs + +import android.testing.AndroidTestingRunner +import android.view.KeyEvent +import android.view.KeyEvent.KEYCODE_DPAD_LEFT +import android.view.View +import androidx.core.util.Consumer +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LeftRightArrowPressedListenerTest : SysuiTestCase() { + + private lateinit var underTest: LeftRightArrowPressedListener + private val callback = + object : Consumer<Int> { + var lastValue: Int? = null + + override fun accept(keyCode: Int?) { + lastValue = keyCode + } + } + + private val view = View(context) + + @Before + fun setUp() { + underTest = LeftRightArrowPressedListener.createAndRegisterListenerForView(view) + underTest.setArrowKeyPressedListener(callback) + } + + @Test + fun shouldTriggerCallback_whenArrowUpReceived_afterArrowDownReceived() { + underTest.sendKey(KeyEvent.ACTION_DOWN, KEYCODE_DPAD_LEFT) + + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isEqualTo(KEYCODE_DPAD_LEFT) + } + + @Test + fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownNotReceived() { + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isNull() + } + + @Test + fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownWasRepeated() { + underTest.sendKeyWithRepeat(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT, repeat = 2) + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isNull() + } + + @Test + fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownReceivedBeforeLosingFocus() { + underTest.sendKey(KeyEvent.ACTION_DOWN, KEYCODE_DPAD_LEFT) + underTest.onFocusChange(view, hasFocus = false) + underTest.onFocusChange(view, hasFocus = true) + + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isNull() + } + + private fun LeftRightArrowPressedListener.sendKey(action: Int, keyCode: Int) { + onKey(view, keyCode, KeyEvent(action, keyCode)) + } + + private fun LeftRightArrowPressedListener.sendKeyWithRepeat( + action: Int, + keyCode: Int, + repeat: Int + ) { + val keyEvent = + KeyEvent( + /* downTime= */ 0L, + /* eventTime= */ 0L, + /* action= */ action, + /* code= */ KEYCODE_DPAD_LEFT, + /* repeat= */ repeat + ) + onKey(view, keyCode, keyEvent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt index db9e548e74c8..8ef3f57103a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt @@ -2,11 +2,13 @@ package com.android.systemui.qs import android.content.Context import android.testing.AndroidTestingRunner -import android.view.KeyEvent import android.view.View import android.widget.Scroller import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.PageIndicator.PageScrollActionListener +import com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT +import com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -22,7 +24,7 @@ import org.mockito.MockitoAnnotations class PagedTileLayoutTest : SysuiTestCase() { @Mock private lateinit var pageIndicator: PageIndicator - @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener> + @Captor private lateinit var captor: ArgumentCaptor<PageScrollActionListener> private lateinit var pageTileLayout: TestPagedTileLayout private lateinit var scroller: Scroller @@ -32,7 +34,7 @@ class PagedTileLayoutTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) pageTileLayout = TestPagedTileLayout(mContext) pageTileLayout.setPageIndicator(pageIndicator) - verify(pageIndicator).setOnKeyListener(captor.capture()) + verify(pageIndicator).setPageScrollActionListener(captor.capture()) setViewWidth(pageTileLayout, width = PAGE_WIDTH) scroller = pageTileLayout.mScroller } @@ -43,28 +45,27 @@ class PagedTileLayoutTest : SysuiTestCase() { } @Test - fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() { + fun scrollsRight_afterRightScrollActionTriggered() { pageTileLayout.currentPageIndex = 0 - sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT) + sendScrollActionEvent(RIGHT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH) } @Test - fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() { + fun scrollsLeft_afterLeftScrollActionTriggered() { pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page - sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT) + sendScrollActionEvent(LEFT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH) } - private fun sendUpEvent(keyCode: Int) { - val event = KeyEvent(KeyEvent.ACTION_UP, keyCode) - captor.value.onKey(pageIndicator, keyCode, event) + private fun sendScrollActionEvent(@PageScrollActionListener.Direction direction: Int) { + captor.value.onScrollActionTriggered(direction) } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index c7118066a276..1cb3bf6c7100 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -393,23 +393,23 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun backgroundAlpha_inSplitShade_followsExpansion_with_0_99_delay() { + fun backgroundAlpha_inSplitShade_followsExpansion_with_0_15_delay() { val underTest = utils.footerActionsViewModel() val floatTolerance = 0.01f underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) - underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.1f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) - underTest.onQuickSettingsExpansionChanged(0.9f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.14f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) - underTest.onQuickSettingsExpansionChanged(0.991f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.235f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.1f) - underTest.onQuickSettingsExpansionChanged(0.995f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.575f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.5f) underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt index 33066d2092b8..9563cebf898b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt @@ -162,13 +162,15 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testStartSettingsActivity_activityLaunched_dialogDismissed() { - `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) - bluetoothTileDialogViewModel.showDialog(context, null) + testScope.runTest { + `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) + bluetoothTileDialogViewModel.showDialog(context, null) - val clickedView = View(context) - bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView) + val clickedView = View(context) + bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView) - verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED) - verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable()) + verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED) + verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable()) + } } } 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 49579f6f46b4..b3e386e69905 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -113,6 +113,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransition import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -125,7 +126,6 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.qs.QSFragmentLegacy; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeAnimationRepository; @@ -355,8 +355,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected FakeKeyguardRepository mFakeKeyguardRepository; protected KeyguardInteractor mKeyguardInteractor; protected ShadeAnimationInteractor mShadeAnimationInteractor; - protected SceneTestUtils mUtils = new SceneTestUtils(this); - protected TestScope mTestScope = mUtils.getTestScope(); + protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + protected TestScope mTestScope = mKosmos.getTestScope(); protected ShadeInteractor mShadeInteractor; protected PowerInteractor mPowerInteractor; protected NotificationPanelViewController.TouchHandler mTouchHandler; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 971c8a3d1c05..a894f877fe3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -73,11 +73,11 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; import com.android.systemui.scene.FakeWindowRootViewComponent; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; @@ -150,8 +150,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { private final Executor mMainExecutor = MoreExecutors.directExecutor(); private final Executor mBackgroundExecutor = MoreExecutors.directExecutor(); - private final SceneTestUtils mUtils = new SceneTestUtils(this); - private final TestScope mTestScope = mUtils.getTestScope(); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private final TestScope mTestScope = mKosmos.getTestScope(); private ShadeInteractor mShadeInteractor; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; @@ -178,15 +178,15 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeShadeRepository shadeRepository = new FakeShadeRepository(); - mScreenOffAnimationController = mUtils.getScreenOffAnimationController(); - mStatusBarStateController = spy(mUtils.getStatusBarStateController()); - PowerInteractor powerInteractor = mUtils.powerInteractor(); + mScreenOffAnimationController = mKosmos.getScreenOffAnimationController(); + mStatusBarStateController = spy(mKosmos.getStatusBarStateController()); + PowerInteractor powerInteractor = mKosmos.getPowerInteractor(); SceneInteractor sceneInteractor = new SceneInteractor( mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mUtils.fakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig()), powerInteractor, mock(SceneLogger.class)); @@ -219,8 +219,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, shadeRepository, @@ -245,8 +245,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mKeyguardSecurityModel, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index ca68fd867b39..cbd4d2bfe377 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -61,6 +61,7 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; @@ -68,7 +69,6 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.qs.QSFragmentLegacy; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; @@ -131,8 +131,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { protected QuickSettingsController mQsController; - protected SceneTestUtils mUtils = new SceneTestUtils(this); - protected TestScope mTestScope = mUtils.getTestScope(); + protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + protected TestScope mTestScope = mKosmos.getTestScope(); @Mock protected Resources mResources; @Mock protected KeyguardBottomAreaView mQsFrame; @@ -203,8 +203,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { public void setup() { MockitoAnnotations.initMocks(this); when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController); - mStatusBarStateController = mUtils.getStatusBarStateController(); - mInteractionJankMonitor = mUtils.getInteractionJankMonitor(); + mStatusBarStateController = mKosmos.getStatusBarStateController(); + mInteractionJankMonitor = mKosmos.getInteractionJankMonitor(); FakeDeviceProvisioningRepository deviceProvisioningRepository = new FakeDeviceProvisioningRepository(); @@ -212,13 +212,13 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); - PowerInteractor powerInteractor = mUtils.powerInteractor(); + PowerInteractor powerInteractor = mKosmos.getPowerInteractor(); SceneInteractor sceneInteractor = new SceneInteractor( mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mUtils.fakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig()), powerInteractor, mock(SceneLogger.class)); @@ -250,8 +250,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mShadeRepository, @@ -276,8 +276,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mock(KeyguardSecurityModel.class), diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt index 215f8b1c462f..c4911a41b4a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt @@ -33,6 +33,8 @@ import com.android.systemui.scene.data.repository.WindowRootViewVisibilityReposi import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.row.NotificationGutsManager import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.DeviceProvisionedController @@ -45,6 +47,7 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import dagger.Lazy +import kotlinx.coroutines.test.StandardTestDispatcher import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -59,6 +62,8 @@ import org.mockito.MockitoAnnotations @SmallTest class ShadeControllerImplTest : SysuiTestCase() { private val executor = FakeExecutor(FakeSystemClock()) + private val testDispatcher = StandardTestDispatcher() + private val activeNotificationsRepository = ActiveNotificationListRepository() @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var keyguardStateController: KeyguardStateController @@ -84,6 +89,7 @@ class ShadeControllerImplTest : SysuiTestCase() { FakeKeyguardRepository(), headsUpManager, PowerInteractorFactory.create().powerInteractor, + ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher) ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java deleted file mode 100644 index e1d928272e16..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package com.android.systemui.statusbar; - -import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; -import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; - -import static com.google.common.truth.Truth.assertThat; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.spy; - -import android.app.ActivityManager; -import android.app.Notification; -import android.os.UserHandle; -import android.service.notification.StatusBarNotification; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.res.R; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import com.android.systemui.statusbar.policy.HeadsUpManagerLogger; -import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.settings.FakeGlobalSettings; -import com.android.systemui.util.time.FakeSystemClock; -import com.android.systemui.util.time.SystemClock; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class AlertingNotificationManagerTest extends SysuiTestCase { - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - - private static final String TEST_PACKAGE_NAME = "test"; - private static final int TEST_UID = 0; - - protected static final int TEST_MINIMUM_DISPLAY_TIME = 400; - protected static final int TEST_AUTO_DISMISS_TIME = 600; - protected static final int TEST_STICKY_AUTO_DISMISS_TIME = 800; - // Number of notifications to use in tests requiring multiple notifications - private static final int TEST_NUM_NOTIFICATIONS = 4; - - protected final FakeGlobalSettings mGlobalSettings = new FakeGlobalSettings(); - protected final FakeSystemClock mSystemClock = new FakeSystemClock(); - protected final FakeExecutor mExecutor = new FakeExecutor(mSystemClock); - - @Mock protected ExpandableNotificationRow mRow; - - static { - assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME); - assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME); - } - - private static class TestableAlertingNotificationManager extends AlertingNotificationManager { - private AlertEntry mLastCreatedEntry; - - private TestableAlertingNotificationManager(SystemClock systemClock, - DelayableExecutor executor) { - super(new HeadsUpManagerLogger(logcatLogBuffer()), systemClock, executor); - mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME; - mAutoDismissTime = TEST_AUTO_DISMISS_TIME; - mStickyForSomeTimeAutoDismissTime = TEST_STICKY_AUTO_DISMISS_TIME; - } - - @Override - protected void onAlertEntryAdded(AlertEntry alertEntry) {} - - @Override - protected void onAlertEntryRemoved(AlertEntry alertEntry) {} - - @Override - protected AlertEntry createAlertEntry() { - mLastCreatedEntry = spy(super.createAlertEntry()); - return mLastCreatedEntry; - } - - @Override - public int getContentFlag() { - return FLAG_CONTENT_VIEW_CONTRACTED; - } - } - - protected AlertingNotificationManager createAlertingNotificationManager() { - return new TestableAlertingNotificationManager(mSystemClock, mExecutor); - } - - protected StatusBarNotification createSbn(int id, Notification n) { - return new StatusBarNotification( - TEST_PACKAGE_NAME /* pkg */, - TEST_PACKAGE_NAME, - id, - null /* tag */, - TEST_UID, - 0 /* initialPid */, - n, - new UserHandle(ActivityManager.getCurrentUser()), - null /* overrideGroupKey */, - 0 /* postTime */); - } - - protected StatusBarNotification createSbn(int id, Notification.Builder n) { - return createSbn(id, n.build()); - } - - protected StatusBarNotification createSbn(int id) { - final Notification.Builder b = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setContentTitle("Title") - .setContentText("Text"); - return createSbn(id, b); - } - - protected NotificationEntry createEntry(int id, Notification n) { - return new NotificationEntryBuilder().setSbn(createSbn(id, n)).build(); - } - - protected NotificationEntry createEntry(int id) { - return new NotificationEntryBuilder().setSbn(createSbn(id)).build(); - } - - - @Test - public void testShowNotification_addsEntry() { - final AlertingNotificationManager alm = createAlertingNotificationManager(); - final NotificationEntry entry = createEntry(/* id = */ 0); - - alm.showNotification(entry); - - assertTrue(alm.isAlerting(entry.getKey())); - assertTrue(alm.hasNotifications()); - assertEquals(entry, alm.getEntry(entry.getKey())); - } - - @Test - public void testShowNotification_autoDismisses() { - final AlertingNotificationManager alm = createAlertingNotificationManager(); - final NotificationEntry entry = createEntry(/* id = */ 0); - - alm.showNotification(entry); - mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME * 3 / 2); - - assertFalse(alm.isAlerting(entry.getKey())); - } - - @Test - public void testRemoveNotification_removeDeferred() { - final AlertingNotificationManager alm = createAlertingNotificationManager(); - final NotificationEntry entry = createEntry(/* id = */ 0); - - alm.showNotification(entry); - - final boolean removedImmediately = alm.removeNotification( - entry.getKey(), /* releaseImmediately = */ false); - assertFalse(removedImmediately); - assertTrue(alm.isAlerting(entry.getKey())); - } - - @Test - public void testRemoveNotification_forceRemove() { - final AlertingNotificationManager alm = createAlertingNotificationManager(); - final NotificationEntry entry = createEntry(/* id = */ 0); - - alm.showNotification(entry); - - final boolean removedImmediately = alm.removeNotification( - entry.getKey(), /* releaseImmediately = */ true); - assertTrue(removedImmediately); - assertFalse(alm.isAlerting(entry.getKey())); - } - - @Test - public void testReleaseAllImmediately() { - final AlertingNotificationManager alm = createAlertingNotificationManager(); - for (int i = 0; i < TEST_NUM_NOTIFICATIONS; i++) { - final NotificationEntry entry = createEntry(i); - entry.setRow(mRow); - alm.showNotification(entry); - } - - alm.releaseAllImmediately(); - - assertEquals(0, alm.getAllEntries().count()); - } - - @Test - public void testCanRemoveImmediately_notShownLongEnough() { - final AlertingNotificationManager alm = createAlertingNotificationManager(); - final NotificationEntry entry = createEntry(/* id = */ 0); - - alm.showNotification(entry); - - // The entry has just been added so we should not remove immediately. - assertFalse(alm.canRemoveImmediately(entry.getKey())); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index 4a365b9ab084..05e866e85112 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -41,10 +41,12 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.data.repository.FakeShadeRepository @@ -56,6 +58,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Share import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import kotlinx.coroutines.flow.emptyFlow import org.junit.Assert.assertEquals @@ -71,17 +74,17 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class StatusBarStateControllerImplTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val testDispatcher = utils.testDispatcher + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val testDispatcher = kosmos.testDispatcher private lateinit var shadeInteractor: ShadeInteractor private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromPrimaryBouncerTransitionInteractor: @@ -131,7 +134,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { FakeKeyguardBouncerRepository(), ConfigurationInteractor(configurationRepository), shadeRepository, - utils::sceneInteractor + { kosmos.sceneInteractor }, ) val keyguardTransitionInteractor = KeyguardTransitionInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt index 605936372e7a..cd744100770e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt @@ -73,7 +73,7 @@ class NotificationLaunchAnimatorControllerTest : SysuiTestCase() { } private fun flagNotificationAsHun() { - `when`(headsUpManager.isAlerting(notificationKey)).thenReturn(true) + `when`(headsUpManager.isHeadsUpEntry(notificationKey)).thenReturn(true) } @Test @@ -151,8 +151,8 @@ class NotificationLaunchAnimatorControllerTest : SysuiTestCase() { .build() assertSame(summary, notification.entry.parent?.summary) - `when`(headsUpManager.isAlerting(notificationKey)).thenReturn(false) - `when`(headsUpManager.isAlerting(summary.key)).thenReturn(true) + `when`(headsUpManager.isHeadsUpEntry(notificationKey)).thenReturn(false) + `when`(headsUpManager.isHeadsUpEntry(summary.key)).thenReturn(true) assertNotSame(GROUP_ALERT_SUMMARY, summary.sbn.notification.groupAlertBehavior) assertNotSame(GROUP_ALERT_SUMMARY, notification.entry.sbn.notification.groupAlertBehavior) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index a8be62b367b4..cd75e0811fff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -148,7 +148,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(remoteInputManager).addActionPressListener(capture()) } given(headsUpManager.allEntries).willAnswer { huns.stream() } - given(headsUpManager.isAlerting(anyString())).willAnswer { invocation -> + given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation -> val key = invocation.getArgument<String>(0) huns.any { entry -> entry.key == key } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index 2e74d119849e..ea5a6e72557a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -140,7 +140,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { .setSummary(mEntry) .build(); - when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(false); + when(mHeadsUpManager.isHeadsUpEntry(mEntry.getKey())).thenReturn(false); // Whenever we invalidate, the pipeline runs again, so we invalidate the state doAnswer(i -> { @@ -373,7 +373,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { setSleepy(false); // WHEN a notification is alerting and visible - when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true); + when(mHeadsUpManager.isHeadsUpEntry(mEntry.getKey())).thenReturn(true); when(mVisibilityLocationProvider.isInVisibleLocation(any(NotificationEntry.class))) .thenReturn(true); @@ -389,7 +389,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { setSleepy(false); // WHEN a notification is alerting but not visible - when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true); + when(mHeadsUpManager.isHeadsUpEntry(mEntry.getKey())).thenReturn(true); when(mVisibilityLocationProvider.isInVisibleLocation(any(NotificationEntry.class))) .thenReturn(false); @@ -537,7 +537,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry)); // GIVEN mEntry is a HUN - when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true); + when(mHeadsUpManager.isHeadsUpEntry(mEntry.getKey())).thenReturn(true); // THEN group + section changes are allowed assertTrue(mNotifStabilityManager.isGroupChangeAllowed(mEntry)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java index 168e782d0481..ff02ef3d4e62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java @@ -28,6 +28,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; + import android.app.Notification; import android.os.Handler; import android.os.Looper; @@ -56,6 +58,8 @@ 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.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository; +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; import com.android.systemui.statusbar.notification.logging.nano.Notifications; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -110,6 +114,11 @@ public class NotificationLoggerTest extends SysuiTestCase { private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); private final PowerInteractor mPowerInteractor = PowerInteractorFactory.create().getPowerInteractor(); + private final ActiveNotificationListRepository mActiveNotificationListRepository = + new ActiveNotificationListRepository(); + private final ActiveNotificationsInteractor mActiveNotificationsInteractor = + new ActiveNotificationsInteractor(mActiveNotificationListRepository, + StandardTestDispatcher(null, null)); private WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor; private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); @@ -123,7 +132,8 @@ public class NotificationLoggerTest extends SysuiTestCase { new WindowRootViewVisibilityRepository(mBarService, mUiBgExecutor), mKeyguardRepository, mHeadsUpManager, - mPowerInteractor); + mPowerInteractor, + mActiveNotificationsInteractor); mWindowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true); mEntry = new NotificationEntryBuilder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java index 8a730cfd7ddd..71613edb8737 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java @@ -42,6 +42,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; + import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; @@ -85,6 +87,8 @@ import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository; +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -156,6 +160,12 @@ public class NotificationGutsManagerTest extends SysuiTestCase { @Mock private UserManager mUserManager; + private final ActiveNotificationListRepository mActiveNotificationListRepository = + new ActiveNotificationListRepository(); + private final ActiveNotificationsInteractor mActiveNotificationsInteractor = + new ActiveNotificationsInteractor(mActiveNotificationListRepository, + StandardTestDispatcher(null, null)); + private WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor; @Before @@ -171,7 +181,8 @@ public class NotificationGutsManagerTest extends SysuiTestCase { new WindowRootViewVisibilityRepository(mBarService, mExecutor), new FakeKeyguardRepository(), mHeadsUpManager, - PowerInteractorFactory.create().getPowerInteractor()); + PowerInteractorFactory.create().getPowerInteractor(), + mActiveNotificationsInteractor); mGutsManager = new NotificationGutsManager( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 88662b60c7d1..89f826b2049d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -66,6 +66,8 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; +import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; @@ -91,6 +93,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback; +import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScrimController; @@ -112,6 +115,8 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import javax.inject.Provider; + /** * Tests for {@link NotificationStackScrollLayoutController}. */ @@ -153,6 +158,9 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationRemoteInputManager mRemoteInputManager; @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; @Mock private ShadeController mShadeController; + @Mock private SceneContainerFlags mSceneContainerFlags; + @Mock private Provider<WindowRootView> mWindowRootView; + @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor; @Mock private InteractionJankMonitor mJankMonitor; private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(), logcatLogBuffer()); @@ -724,6 +732,9 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSeenNotificationsInteractor, mViewBinder, mShadeController, + mSceneContainerFlags, + mWindowRootView, + mNotificationStackAppearanceInteractor, mJankMonitor, mStackLogger, mLogger, 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 a1721208b2f2..4afcc8c9da43 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 @@ -20,6 +20,7 @@ import static android.view.View.GONE; import static android.view.WindowInsets.Type.ime; import static com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION; +import static com.android.systemui.Flags.FLAG_SCENE_CONTAINER; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.RUBBER_BAND_FACTOR_NORMAL; @@ -37,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; @@ -51,6 +53,7 @@ import static org.mockito.Mockito.when; import android.graphics.Insets; import android.graphics.Rect; +import android.os.SystemClock; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; @@ -74,6 +77,7 @@ import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; 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.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.EmptyShadeView; @@ -93,11 +97,13 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -947,6 +953,78 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(runnable).run(); } + @Test + public void testDispatchTouchEvent_sceneContainerDisabled() { + Assume.assumeFalse(SceneContainerFlag.isEnabled()); + + MotionEvent event = MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, + 0, + 0, + 0 + ); + + mStackScroller.dispatchTouchEvent(event); + + verify(mStackScrollLayoutController, never()).sendTouchToSceneFramework(any()); + } + + @Test + public void testDispatchTouchEvent_sceneContainerEnabled() { + Assume.assumeTrue(SceneContainerFlag.isEnabled()); + mStackScroller.setIsBeingDragged(true); + + MotionEvent moveEvent = MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, + 0, + 0, + 0 + ); + MotionEvent syntheticDownEvent = moveEvent.copy(); + syntheticDownEvent.setAction(MotionEvent.ACTION_DOWN); + mStackScroller.dispatchTouchEvent(moveEvent); + + verify(mStackScrollLayoutController).sendTouchToSceneFramework(argThat( + new MotionEventMatcher(syntheticDownEvent))); + + mStackScroller.dispatchTouchEvent(moveEvent); + + verify(mStackScrollLayoutController).sendTouchToSceneFramework(moveEvent); + } + + @Test + @EnableFlags(FLAG_SCENE_CONTAINER) + public void testDispatchTouchEvent_sceneContainerEnabled_actionUp() { + Assume.assumeTrue(SceneContainerFlag.isEnabled()); + mStackScroller.setIsBeingDragged(true); + + MotionEvent upEvent = MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, + 0, + 0, + 0 + ); + MotionEvent syntheticDownEvent = upEvent.copy(); + syntheticDownEvent.setAction(MotionEvent.ACTION_DOWN); + + mStackScroller.dispatchTouchEvent(upEvent); + + verify(mStackScrollLayoutController, atLeastOnce()).sendTouchToSceneFramework(argThat( + new MotionEventMatcher(syntheticDownEvent))); + + mStackScroller.dispatchTouchEvent(upEvent); + + verify(mStackScrollLayoutController, atLeastOnce()).sendTouchToSceneFramework(argThat( + new MotionEventMatcher(upEvent))); + assertFalse(mStackScroller.getIsBeingDragged()); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. @@ -993,4 +1071,21 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { /* metaState= */0 ); } + + private static class MotionEventMatcher implements ArgumentMatcher<MotionEvent> { + private final MotionEvent mLeftEvent; + + MotionEventMatcher(MotionEvent leftEvent) { + mLeftEvent = leftEvent; + } + + @Override + public boolean matches(MotionEvent right) { + return mLeftEvent.getActionMasked() == right.getActionMasked() + && mLeftEvent.getDownTime() == right.getDownTime() + && mLeftEvent.getEventTime() == right.getEventTime() + && mLeftEvent.getX() == right.getX() + && mLeftEvent.getY() == right.getY(); + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 9f15b05f8f02..a824bc4f803f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -484,6 +484,46 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun translationYUpdatesOnKeyguard() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY) + + configurationRepository.setDimensionPixelSize( + R.dimen.keyguard_translate_distance_on_swipe_up, + -100 + ) + configurationRepository.onAnyConfigurationChange() + + // legacy expansion means the user is swiping up, usually for the bouncer + shadeRepository.setLegacyShadeExpansion(0.5f) + + showLockscreen() + + // The translation values are negative + assertThat(translationY).isLessThan(0f) + } + + @Test + fun translationYDoesNotUpdateWhenShadeIsExpanded() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY) + + configurationRepository.setDimensionPixelSize( + R.dimen.keyguard_translate_distance_on_swipe_up, + -100 + ) + configurationRepository.onAnyConfigurationChange() + + // legacy expansion means the user is swiping up, usually for the bouncer but also for + // shade collapsing + shadeRepository.setLegacyShadeExpansion(0.5f) + + showLockscreenWithShadeExpanded() + + assertThat(translationY).isEqualTo(0f) + } + + @Test fun updateBounds_fromKeyguardRoot() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java index 2b1f5fca8feb..c350de21dda5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java @@ -35,13 +35,12 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.shade.domain.interactor.ShadeInteractor; -import com.android.systemui.statusbar.AlertingNotificationManager; -import com.android.systemui.statusbar.AlertingNotificationManagerTest; import com.android.systemui.statusbar.NotificationShadeWindowController; 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.policy.AccessibilityManagerWrapper; +import com.android.systemui.statusbar.policy.BaseHeadsUpManagerTest; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.HeadsUpManagerLogger; @@ -64,7 +63,7 @@ import kotlinx.coroutines.flow.StateFlowKt; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { +public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); private final HeadsUpManagerLogger mHeadsUpManagerLogger = new HeadsUpManagerLogger( @@ -137,11 +136,6 @@ public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { ); } - @Override - protected AlertingNotificationManager createAlertingNotificationManager() { - return createHeadsUpManagerPhone(); - } - @Before public void setUp() { when(mShadeInteractor.isAnyExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false)); @@ -179,7 +173,7 @@ public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { /* releaseImmediately = */ false); assertTrue(removedImmediately); - assertFalse(hmp.isAlerting(entry.getKey())); + assertFalse(hmp.isHeadsUpEntry(entry.getKey())); } @Test @@ -218,6 +212,6 @@ public class HeadsUpManagerPhoneTest extends AlertingNotificationManagerTest { hmp.extendHeadsUp(); mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME + hmp.mExtensionTime / 2); - assertTrue(hmp.isAlerting(entry.getKey())); + assertTrue(hmp.isHeadsUpEntry(entry.getKey())); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt index 91cbc3274900..7362e342f321 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt @@ -24,9 +24,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.DevicePostureController @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POST import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.tuner.TunerService import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -60,8 +61,8 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardBypassControllerTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val featureFlags = FakeFeatureFlags() private val shadeRepository = FakeShadeRepository() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 5fa7f13ecd6e..2d120cd9b13f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -60,10 +60,10 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractorFactory; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.statusbar.CommandQueue; @@ -149,7 +149,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private final TestScope mTestScope = TestScopeProvider.getTestScope(); private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); - private final SceneTestUtils mSceneTestUtils = new SceneTestUtils(this); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private KeyguardInteractor mKeyguardInteractor; private KeyguardStatusBarViewModel mViewModel; @@ -166,11 +166,11 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mKeyguardRepository, mCommandQueue, PowerInteractorFactory.create().getPowerInteractor(), - mSceneTestUtils.getFakeSceneContainerFlags(), + mKosmos.getFakeSceneContainerFlags(), new FakeKeyguardBouncerRepository(), new ConfigurationInteractor(new FakeConfigurationRepository()), new FakeShadeRepository(), - () -> mSceneTestUtils.sceneInteractor()); + () -> mKosmos.getSceneInteractor()); mViewModel = new KeyguardStatusBarViewModel( mTestScope.getBackgroundScope(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 7d91e8baa92c..02e6fd5a9d6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -35,7 +35,6 @@ import android.telephony.satellite.SatelliteModemStateCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState @@ -88,7 +87,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { Optional.empty(), dispatcher, testScope.backgroundScope, - FakeLogBuffer.Factory.create(), systemClock, ) @@ -102,22 +100,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test - fun satelliteManagerThrows_doesNotCrash() = - testScope.runTest { - setupDefaultRepo() - - whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any())) - .thenThrow(SatelliteException(13)) - - val conn by collectLastValue(underTest.connectionState) - val strength by collectLastValue(underTest.signalStrength) - - // Flows have not emitted, we haven't crashed - assertThat(conn).isNull() - assertThat(strength).isNull() - } - - @Test fun connectionState_mapsFromSatelliteModemState() = testScope.runTest { setupDefaultRepo() @@ -398,7 +380,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { if (satMan != null) Optional.of(satMan) else Optional.empty(), dispatcher, testScope.backgroundScope, - FakeLogBuffer.Factory.create(), systemClock, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt deleted file mode 100644 index 21c038ad476d..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy -import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository -import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor -import com.android.systemui.util.mockito.mock -import com.google.common.truth.Truth.assertThat -import kotlin.test.Test -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.mockito.MockitoAnnotations - -@SmallTest -class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { - private lateinit var underTest: DeviceBasedSatelliteViewModel - private lateinit var interactor: DeviceBasedSatelliteInteractor - - private val repo = FakeDeviceBasedSatelliteRepository() - private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) - - private val testScope = TestScope() - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - interactor = - DeviceBasedSatelliteInteractor( - repo, - mobileIconsInteractor, - testScope.backgroundScope, - ) - - underTest = - DeviceBasedSatelliteViewModel( - interactor, - testScope.backgroundScope, - ) - } - - @Test - fun icon_nullWhenShouldNotShow_satelliteNotAllowed() = - testScope.runTest { - val latest by collectLastValue(underTest.icon) - - // GIVEN satellite is not allowed - repo.isSatelliteAllowedForCurrentLocation.value = false - - // GIVEN all icons are OOS - val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = false - - // THEN icon is null because we should not be showing it - assertThat(latest).isNull() - } - - @Test - fun icon_nullWhenShouldNotShow_notAllOos() = - testScope.runTest { - val latest by collectLastValue(underTest.icon) - - // GIVEN satellite is allowed - repo.isSatelliteAllowedForCurrentLocation.value = true - - // GIVEN all icons are not OOS - val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = true - - // THEN icon is null because we have service - assertThat(latest).isNull() - } - - @Test - fun icon_satelliteIsOff() = - testScope.runTest { - val latest by collectLastValue(underTest.icon) - - // GIVEN satellite is allowed - repo.isSatelliteAllowedForCurrentLocation.value = true - - // GIVEN all icons are OOS - val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = false - - // THEN icon is null because we have service - assertThat(latest).isInstanceOf(Icon::class.java) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt deleted file mode 100644 index ca9df57e8798..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.pipeline.shared.ui.view - -import android.graphics.Rect -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.StatusBarIconView -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Being a simple subclass of [ModernStatusBarView], use the same basic test cases to verify the - * root behavior, and add testing for the new [SingleBindableStatusBarIconView.withDefaultBinding] - * method. - */ -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -class SingleBindableStatusBarIconViewTest : SysuiTestCase() { - private lateinit var binding: SingleBindableStatusBarIconViewBinding - - // Visibility is outsourced to view-models. This simulates it - private var isVisible = true - private var visibilityFn: () -> Boolean = { isVisible } - - @Test - fun initView_hasCorrectSlot() { - val view = createAndInitView() - - assertThat(view.slot).isEqualTo(SLOT_NAME) - } - - @Test - fun getVisibleState_icon_returnsIcon() { - val view = createAndInitView() - - view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_ICON) - } - - @Test - fun getVisibleState_dot_returnsDot() { - val view = createAndInitView() - - view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_DOT) - } - - @Test - fun getVisibleState_hidden_returnsHidden() { - val view = createAndInitView() - - view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_HIDDEN) - } - - @Test - fun onDarkChanged_bindingReceivesIconAndDecorTint() { - val view = createAndInitView() - - view.onDarkChangedWithContrast(arrayListOf(), 0x12345678, 0x12344321) - - assertThat(binding.iconTint).isEqualTo(0x12345678) - assertThat(binding.decorTint).isEqualTo(0x12345678) - } - - @Test - fun setStaticDrawableColor_bindingReceivesIconTint() { - val view = createAndInitView() - - view.setStaticDrawableColor(0x12345678, 0x12344321) - - assertThat(binding.iconTint).isEqualTo(0x12345678) - } - - @Test - fun setDecorColor_bindingReceivesDecorColor() { - val view = createAndInitView() - - view.setDecorColor(0x23456789) - - assertThat(binding.decorTint).isEqualTo(0x23456789) - } - - @Test - fun isIconVisible_usesBinding_true() { - val view = createAndInitView() - - isVisible = true - - assertThat(view.isIconVisible).isEqualTo(true) - } - - @Test - fun isIconVisible_usesBinding_false() { - val view = createAndInitView() - - isVisible = false - - assertThat(view.isIconVisible).isEqualTo(false) - } - - @Test - fun getDrawingRect_takesTranslationIntoAccount() { - val view = createAndInitView() - - view.translationX = 50f - view.translationY = 60f - - val drawingRect = Rect() - view.getDrawingRect(drawingRect) - - assertThat(drawingRect.left).isEqualTo(view.left + 50) - assertThat(drawingRect.right).isEqualTo(view.right + 50) - assertThat(drawingRect.top).isEqualTo(view.top + 60) - assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60) - } - - private fun createAndInitView(): SingleBindableStatusBarIconView { - val view = SingleBindableStatusBarIconView.createView(context) - binding = SingleBindableStatusBarIconView.withDefaultBinding(view, visibilityFn) {} - view.initView(SLOT_NAME) { binding } - return view - } - - companion object { - private const val SLOT_NAME = "test_slot" - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java index 6a3b2c3ea55c..4c824c0d130a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.policy; import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler; import static com.google.common.truth.Truth.assertThat; @@ -36,12 +37,15 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; import android.content.Context; import android.content.Intent; import android.graphics.Region; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -51,13 +55,16 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; -import com.android.systemui.statusbar.AlertingNotificationManager; -import com.android.systemui.statusbar.AlertingNotificationManagerTest; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.settings.FakeGlobalSettings; import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; import org.junit.Rule; @@ -70,10 +77,12 @@ import org.mockito.junit.MockitoRule; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { +public class BaseHeadsUpManagerTest extends SysuiTestCase { @Rule public MockitoRule rule = MockitoJUnit.rule(); + private static final String TEST_PACKAGE_NAME = "BaseHeadsUpManagerTest"; + private static final int TEST_TOUCH_ACCEPTANCE_TIME = 200; private static final int TEST_A11Y_AUTO_DISMISS_TIME = 1_000; @@ -81,6 +90,20 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { private final HeadsUpManagerLogger mLogger = spy(new HeadsUpManagerLogger(logcatLogBuffer())); @Mock private AccessibilityManagerWrapper mAccessibilityMgr; + private static final int TEST_UID = 0; + + protected static final int TEST_MINIMUM_DISPLAY_TIME = 400; + protected static final int TEST_AUTO_DISMISS_TIME = 600; + protected static final int TEST_STICKY_AUTO_DISMISS_TIME = 800; + // Number of notifications to use in tests requiring multiple notifications + private static final int TEST_NUM_NOTIFICATIONS = 4; + + protected final FakeGlobalSettings mGlobalSettings = new FakeGlobalSettings(); + protected final FakeSystemClock mSystemClock = new FakeSystemClock(); + protected final FakeExecutor mExecutor = new FakeExecutor(mSystemClock); + + @Mock protected ExpandableNotificationRow mRow; + static { assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME); assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME); @@ -88,6 +111,9 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { } private final class TestableHeadsUpManager extends BaseHeadsUpManager { + + private HeadsUpEntry mLastCreatedEntry; + TestableHeadsUpManager(Context context, HeadsUpManagerLogger logger, DelayableExecutor executor, @@ -97,10 +123,23 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { UiEventLogger uiEventLogger) { super(context, logger, mockExecutorHandler(executor), globalSettings, systemClock, executor, accessibilityManagerWrapper, uiEventLogger); + mTouchAcceptanceDelay = TEST_TOUCH_ACCEPTANCE_TIME; mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME; mAutoDismissTime = TEST_AUTO_DISMISS_TIME; mStickyForSomeTimeAutoDismissTime = TEST_STICKY_AUTO_DISMISS_TIME; + + } + + @Override + protected HeadsUpEntry createHeadsUpEntry() { + mLastCreatedEntry = spy(super.createHeadsUpEntry()); + return mLastCreatedEntry; + } + + @Override + public int getContentFlag() { + return FLAG_CONTENT_VIEW_CONTRACTED; } // The following are only implemented by HeadsUpManagerPhone. If you need them, use that. @@ -173,16 +212,46 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { } } + protected StatusBarNotification createSbn(int id, Notification n) { + return new StatusBarNotification( + TEST_PACKAGE_NAME /* pkg */, + TEST_PACKAGE_NAME, + id, + null /* tag */, + TEST_UID, + 0 /* initialPid */, + n, + new UserHandle(ActivityManager.getCurrentUser()), + null /* overrideGroupKey */, + 0 /* postTime */); + } + + protected StatusBarNotification createSbn(int id, Notification.Builder n) { + return createSbn(id, n.build()); + } + + protected StatusBarNotification createSbn(int id) { + final Notification.Builder b = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setContentTitle("Title") + .setContentText("Text"); + return createSbn(id, b); + } + + protected NotificationEntry createEntry(int id, Notification n) { + return new NotificationEntryBuilder().setSbn(createSbn(id, n)).build(); + } + + protected NotificationEntry createEntry(int id) { + return new NotificationEntryBuilder().setSbn(createSbn(id)).build(); + } + + private BaseHeadsUpManager createHeadsUpManager() { return new TestableHeadsUpManager(mContext, mLogger, mExecutor, mGlobalSettings, mSystemClock, mAccessibilityMgr, mUiEventLoggerFake); } - @Override - protected AlertingNotificationManager createAlertingNotificationManager() { - return createHeadsUpManager(); - } - private NotificationEntry createStickyEntry(int id) { final Notification notif = new Notification.Builder(mContext, "") .setSmallIcon(R.drawable.ic_person) @@ -224,6 +293,79 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { } } + @Test + public void testShowNotification_addsEntry() { + final BaseHeadsUpManager alm = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); + + assertTrue(alm.isHeadsUpEntry(entry.getKey())); + assertTrue(alm.hasNotifications()); + assertEquals(entry, alm.getEntry(entry.getKey())); + } + + @Test + public void testShowNotification_autoDismisses() { + final BaseHeadsUpManager alm = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); + mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME * 3 / 2); + + assertFalse(alm.isHeadsUpEntry(entry.getKey())); + } + + @Test + public void testRemoveNotification_removeDeferred() { + final BaseHeadsUpManager alm = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); + + final boolean removedImmediately = alm.removeNotification( + entry.getKey(), /* releaseImmediately = */ false); + assertFalse(removedImmediately); + assertTrue(alm.isHeadsUpEntry(entry.getKey())); + } + + @Test + public void testRemoveNotification_forceRemove() { + final BaseHeadsUpManager alm = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); + + final boolean removedImmediately = alm.removeNotification( + entry.getKey(), /* releaseImmediately = */ true); + assertTrue(removedImmediately); + assertFalse(alm.isHeadsUpEntry(entry.getKey())); + } + + @Test + public void testReleaseAllImmediately() { + final BaseHeadsUpManager alm = createHeadsUpManager(); + for (int i = 0; i < TEST_NUM_NOTIFICATIONS; i++) { + final NotificationEntry entry = createEntry(i); + entry.setRow(mRow); + alm.showNotification(entry); + } + + alm.releaseAllImmediately(); + + assertEquals(0, alm.getAllEntries().count()); + } + + @Test + public void testCanRemoveImmediately_notShownLongEnough() { + final BaseHeadsUpManager alm = createHeadsUpManager(); + final NotificationEntry entry = createEntry(/* id = */ 0); + + alm.showNotification(entry); + + // The entry has just been added so we should not remove immediately. + assertFalse(alm.canRemoveImmediately(entry.getKey())); + } @Test public void testHunRemovedLogging() { @@ -233,7 +375,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { BaseHeadsUpManager.HeadsUpEntry.class); headsUpEntry.mEntry = notifEntry; - hum.onAlertEntryRemoved(headsUpEntry); + hum.onEntryRemoved(headsUpEntry); verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry)); } @@ -286,7 +428,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { hum.showNotification(entry); mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); } @@ -300,7 +442,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2); - assertFalse(hum.isAlerting(entry.getKey())); + assertFalse(hum.isHeadsUpEntry(entry.getKey())); } @@ -314,7 +456,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); } @@ -327,7 +469,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { hum.showNotification(entry); mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); } @@ -341,7 +483,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); } @@ -355,7 +497,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); } @@ -370,11 +512,11 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { final boolean removedImmediately = hum.removeNotification( entry.getKey(), /* releaseImmediately = */ false); assertFalse(removedImmediately); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); mSystemClock.advanceTime((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2); - assertFalse(hum.isAlerting(entry.getKey())); + assertFalse(hum.isHeadsUpEntry(entry.getKey())); } @@ -387,12 +529,12 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { hum.showNotification(entry); mSystemClock.advanceTime((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2); - assertTrue(hum.isAlerting(entry.getKey())); + assertTrue(hum.isHeadsUpEntry(entry.getKey())); final boolean removedImmediately = hum.removeNotification( entry.getKey(), /* releaseImmediately = */ false); assertTrue(removedImmediately); - assertFalse(hum.isAlerting(entry.getKey())); + assertFalse(hum.isHeadsUpEntry(entry.getKey())); } @@ -406,7 +548,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { final boolean removedImmediately = hum.removeNotification( entry.getKey(), /* releaseImmediately = */ true); assertTrue(removedImmediately); - assertFalse(hum.isAlerting(entry.getKey())); + assertFalse(hum.isHeadsUpEntry(entry.getKey())); } @@ -560,7 +702,7 @@ public class BaseHeadsUpManagerTest extends AlertingNotificationManagerTest { // the notification and then updates it; in order to not log twice, the entry needs // to have a functional ExpandableNotificationRow that can keep track of whether it's // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit. - hum.onAlertEntryAdded(entryToPin); + hum.onEntryAdded(entryToPin); assertEquals(1, mUiEventLoggerFake.numLogs()); assertEquals(BaseHeadsUpManager.NotificationPeekEvent.NOTIFICATION_PEEK.getId(), 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 5f9c096b1f3c..ca0e526bbc30 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 @@ -25,20 +25,22 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -47,20 +49,20 @@ import org.mockito.Mockito.verify @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class KeyguardStatusBarViewModelTest : SysuiTestCase() { - private val testScope = TestScope() - private val sceneTestUtils = SceneTestUtils(this) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val keyguardRepository = FakeKeyguardRepository() private val keyguardInteractor = KeyguardInteractor( keyguardRepository, mock<CommandQueue>(), PowerInteractorFactory.create().powerInteractor, - sceneTestUtils.fakeSceneContainerFlags, + kosmos.fakeSceneContainerFlags, FakeKeyguardBouncerRepository(), ConfigurationInteractor(FakeConfigurationRepository()), FakeShadeRepository(), ) { - sceneTestUtils.sceneInteractor() + kosmos.sceneInteractor } private val keyguardStatusBarInteractor = KeyguardStatusBarInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt index 5b4f4d37214f..95c934e988e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProviderTest.kt @@ -88,6 +88,30 @@ class ScopedUnfoldTransitionProgressProviderTest : SysuiTestCase() { } @Test + fun setReadyToHandleTransition_whileTransitionRunning_fromBgThread_propagatesCallbacks() = + testScope.runTest { + runBlockingInBg { rootProvider.onTransitionStarted() } + + runBlockingInBg { + // This causes the transition started callback to be propagated immediately, without + // the need to switch thread (as we're already in the correct one). We don't need a + // sync barrier on the bg thread as in + // setReadyToHandleTransition_whileTransitionRunning_propagatesCallbacks here. + scopedProvider.setReadyToHandleTransition(true) + } + + listener.assertStarted() + + runBlockingInBg { rootProvider.onTransitionProgress(1f) } + + listener.assertLastProgress(1f) + + runBlockingInBg { rootProvider.onTransitionFinished() } + + listener.assertNotStarted() + } + + @Test fun setReadyToHandleTransition_beforeAnyCallback_doesNotCrash() { testScope.runTest { scopedProvider.setReadyToHandleTransition(true) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt index e6b9d9b09246..b6a033a7c5f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt @@ -40,13 +40,18 @@ import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Text import com.android.systemui.coroutines.collectLastValue 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.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.telephonyInteractor +import com.android.systemui.testKosmos import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.source.UserRecord @@ -98,8 +103,8 @@ class UserSwitcherInteractorTest : SysuiTestCase() { @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var spyContext: Context private lateinit var userRepository: FakeUserRepository private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies @@ -121,16 +126,17 @@ class UserSwitcherInteractorTest : SysuiTestCase() { SUPERVISED_USER_CREATION_APP_PACKAGE, ) - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, false) mSetFlagsRule.enableFlags(AConfigFlags.FLAG_SWITCH_USER_ON_BG) spyContext = spy(context) - keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.fakeFeatureFlags) + keyguardReply = + KeyguardInteractorFactory.create(featureFlags = kosmos.fakeFeatureFlagsClassic) keyguardRepository = keyguardReply.repository userRepository = FakeUserRepository() refreshUsersScheduler = RefreshUsersScheduler( applicationScope = testScope.backgroundScope, - mainDispatcher = utils.testDispatcher, + mainDispatcher = kosmos.testDispatcher, repository = userRepository, ) } @@ -363,7 +369,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun actions_deviceUnlocked_fullScreen() { createUserInteractor() testScope.runTest { - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) @@ -447,7 +453,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() { createUserInteractor() testScope.runTest { - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -640,7 +646,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { val refreshUsersCallCount = userRepository.refreshUsersCallCount - utils.telephonyRepository.setCallState(1) + kosmos.fakeTelephonyRepository.setCallState(1) runCurrent() assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) @@ -792,7 +798,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun userRecordsFullScreen() { createUserInteractor() testScope.runTest { - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 3, includeGuest = false) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) userRepository.setUserInfos(userInfos) @@ -901,7 +907,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() { createUserInteractor() testScope.runTest { - utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val expandable = mock<Expandable>() underTest.showUserSwitcher(expandable) @@ -1116,19 +1122,19 @@ class UserSwitcherInteractorTest : SysuiTestCase() { manager = manager, headlessSystemUserMode = headlessSystemUserMode, applicationScope = testScope.backgroundScope, - telephonyInteractor = utils.telephonyInteractor(), + telephonyInteractor = kosmos.telephonyInteractor, broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, - backgroundDispatcher = utils.testDispatcher, - mainDispatcher = utils.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = GuestUserInteractor( applicationContext = spyContext, applicationScope = testScope.backgroundScope, - mainDispatcher = utils.testDispatcher, - backgroundDispatcher = utils.testDispatcher, + mainDispatcher = kosmos.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, manager = manager, repository = userRepository, deviceProvisionedController = deviceProvisionedController, @@ -1139,7 +1145,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { resetOrExitSessionReceiver = resetOrExitSessionReceiver, ), uiEventLogger = uiEventLogger, - featureFlags = utils.fakeFeatureFlags, + featureFlags = kosmos.fakeFeatureFlagsClassic, userRestrictionChecker = mock(), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 8920d4df2a91..1ed045ff6546 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -117,11 +117,11 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.scene.FakeWindowRootViewComponent; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; @@ -356,8 +356,8 @@ public class BubblesTest extends SysuiTestCase { @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper; - private final SceneTestUtils mUtils = new SceneTestUtils(this); - private final TestScope mTestScope = mUtils.getTestScope(); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private final TestScope mTestScope = mKosmos.getTestScope(); private ShadeInteractor mShadeInteractor; private ShellTaskOrganizer mShellTaskOrganizer; private TaskViewTransitions mTaskViewTransitions; @@ -408,8 +408,8 @@ public class BubblesTest extends SysuiTestCase { FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); PowerInteractor powerInteractor = new PowerInteractor( - mUtils.getPowerRepository(), - mUtils.falsingCollector(), + mKosmos.getPowerRepository(), + mKosmos.getFalsingCollector(), mock(ScreenOffAnimationController.class), mStatusBarStateController); @@ -417,7 +417,7 @@ public class BubblesTest extends SysuiTestCase { mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mUtils.fakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig()), powerInteractor, mock(SceneLogger.class)); @@ -449,8 +449,8 @@ public class BubblesTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, shadeRepository, @@ -475,8 +475,8 @@ public class BubblesTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mock(KeyguardSecurityModel.class), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt index 42ec8fed0127..f192de23fecc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -26,12 +26,24 @@ class FakePromptRepository : PromptRepository { private val _isConfirmationRequired = MutableStateFlow(false) override val isConfirmationRequired = _isConfirmationRequired.asStateFlow() + private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null) + override val opPackageName = _opPackageName.asStateFlow() + override fun setPrompt( promptInfo: PromptInfo, userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, - ) = setPrompt(promptInfo, userId, gatekeeperChallenge, kind, forceConfirmation = false) + opPackageName: String, + ) = + setPrompt( + promptInfo, + userId, + gatekeeperChallenge, + kind, + forceConfirmation = false, + opPackageName = opPackageName + ) fun setPrompt( promptInfo: PromptInfo, @@ -39,12 +51,14 @@ class FakePromptRepository : PromptRepository { gatekeeperChallenge: Long?, kind: PromptKind, forceConfirmation: Boolean = false, + opPackageName: String? = null, ) { _promptInfo.value = promptInfo _userId.value = userId _challenge.value = gatekeeperChallenge _kind.value = kind _isConfirmationRequired.value = promptInfo.isConfirmationRequested || forceConfirmation + _opPackageName.value = opPackageName } override fun unsetPrompt() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryKosmos.kt new file mode 100644 index 000000000000..79107cc818ba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.communal.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.communalPrefsRepository: CommunalPrefsRepository by + Kosmos.Fixture { fakeCommunalPrefsRepository } +val Kosmos.fakeCommunalPrefsRepository by Kosmos.Fixture { FakeCommunalPrefsRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt index 1f5af5c38491..482d60ce8adf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt @@ -18,7 +18,10 @@ package com.android.systemui.communal.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope -val Kosmos.fakeCommunalRepository by Fixture { FakeCommunalRepository() } +val Kosmos.fakeCommunalRepository by Fixture { + FakeCommunalRepository(applicationScope = applicationCoroutineScope) +} val Kosmos.communalRepository by Fixture<CommunalRepository> { fakeCommunalRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryKosmos.kt new file mode 100644 index 000000000000..f7665fbd1136 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.communalTutorialRepository by + Kosmos.Fixture<CommunalTutorialRepository> { fakeCommunalTutorialRepository } +val Kosmos.fakeCommunalTutorialRepository by Kosmos.Fixture { FakeCommunalTutorialRepository() } 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 new file mode 100644 index 000000000000..d3ed58bf5be0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Fake implementation of [CommunalPrefsRepository] */ +class FakeCommunalPrefsRepository : CommunalPrefsRepository { + private val _isCtaDismissed = MutableStateFlow(false) + override val isCtaDismissed: Flow<Boolean> = _isCtaDismissed.asStateFlow() + + override suspend fun setCtaDismissedForCurrentUser() { + _isCtaDismissed.value = true + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index e82cae45c8f0..20fa545d54e0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -2,24 +2,21 @@ package com.android.systemui.communal.data.repository import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState -import com.android.systemui.dagger.qualifiers.Background import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.test.TestScope /** Fake implementation of [CommunalRepository]. */ @OptIn(ExperimentalCoroutinesApi::class) class FakeCommunalRepository( - @Background applicationScope: CoroutineScope = TestScope(), - override var isCommunalEnabled: Boolean = false, + applicationScope: CoroutineScope, + override var isCommunalEnabled: Boolean = true, override val desiredScene: MutableStateFlow<CommunalSceneKey> = MutableStateFlow(CommunalSceneKey.DEFAULT), ) : CommunalRepository { @@ -53,12 +50,4 @@ class FakeCommunalRepository( fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) { _isCommunalHubShowing.value = isCommunalHubShowing } - - private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) - override val isCtaTileInViewModeVisible: Flow<Boolean> = - _isCtaTileInViewModeVisible.asStateFlow() - - override fun setCtaTileInViewModeVisibility(isVisible: Boolean) { - _isCtaTileInViewModeVisible.value = isVisible - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 397dc1a464bd..d0c2d4b82fb3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -35,4 +35,13 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : } } } + + private var isHostActive = false + override fun updateAppWidgetHostActive(active: Boolean) { + isHostActive = active + } + + fun isHostActive(): Boolean { + return isHostActive + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt index 95ff889177b8..1753ca05347a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt @@ -17,11 +17,12 @@ package com.android.systemui.communal.domain.interactor -import android.appwidget.AppWidgetHost import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -42,7 +43,8 @@ object CommunalInteractorFactory { mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(), smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), - appWidgetHost: AppWidgetHost = mock(), + communalPrefsRepository: FakeCommunalPrefsRepository = FakeCommunalPrefsRepository(), + appWidgetHost: CommunalAppWidgetHost = mock(), editWidgetsActivityStarter: EditWidgetsActivityStarter = mock(), ): WithDependencies { val withDeps = @@ -55,6 +57,7 @@ object CommunalInteractorFactory { testScope, communalRepository, widgetRepository, + communalPrefsRepository, mediaRepository, smartspaceRepository, tutorialRepository, @@ -64,8 +67,10 @@ object CommunalInteractorFactory { appWidgetHost, editWidgetsActivityStarter, CommunalInteractor( + testScope.backgroundScope, communalRepository, widgetRepository, + communalPrefsRepository, mediaRepository, smartspaceRepository, withDeps.keyguardInteractor, @@ -79,13 +84,14 @@ object CommunalInteractorFactory { val testScope: TestScope, val communalRepository: FakeCommunalRepository, val widgetRepository: FakeCommunalWidgetRepository, + val communalPrefsRepository: FakeCommunalPrefsRepository, val mediaRepository: FakeCommunalMediaRepository, val smartspaceRepository: FakeSmartspaceRepository, val tutorialRepository: FakeCommunalTutorialRepository, val keyguardRepository: FakeKeyguardRepository, val keyguardInteractor: KeyguardInteractor, val tutorialInteractor: CommunalTutorialInteractor, - val appWidgetHost: AppWidgetHost, + val appWidgetHost: CommunalAppWidgetHost, val editWidgetsActivityStarter: EditWidgetsActivityStarter, val communalInteractor: CommunalInteractor, ) 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 649b373258ef..65579a6d9ddf 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 @@ -17,19 +17,23 @@ package com.android.systemui.communal.domain.interactor import com.android.systemui.communal.data.repository.communalMediaRepository +import com.android.systemui.communal.data.repository.communalPrefsRepository import com.android.systemui.communal.data.repository.communalRepository import com.android.systemui.communal.data.repository.communalWidgetRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.smartspace.data.repository.smartspaceRepository import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { CommunalInteractor( + applicationScope = applicationCoroutineScope, communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, + communalPrefsRepository = communalPrefsRepository, smartspaceRepository = smartspaceRepository, appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorFactory.kt index e5cadabfc1c7..3ff2a3ed5145 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorFactory.kt @@ -32,7 +32,7 @@ object CommunalTutorialInteractorFactory { communalTutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), communalRepository: FakeCommunalRepository = - FakeCommunalRepository(isCommunalEnabled = true), + FakeCommunalRepository(applicationScope = testScope.backgroundScope), keyguardRepository: FakeKeyguardRepository = FakeKeyguardRepository(), keyguardInteractor: KeyguardInteractor = KeyguardInteractorFactory.create( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 975db3b179ac..59f56dd18f0e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -127,6 +127,9 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val ambientIndicationVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) + private val _isEncryptedOrLockdown = MutableStateFlow(true) + override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } @@ -247,6 +250,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override fun setKeyguardAlpha(alpha: Float) { _keyguardAlpha.value = alpha } + + fun setIsEncryptedOrLockdown(value: Boolean) { + _isEncryptedOrLockdown.value = value + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt index 59f0ec3cd3a5..d8f0cec54596 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt @@ -16,25 +16,32 @@ package com.android.systemui.keyguard.domain.interactor -import android.appwidget.AppWidgetHost import com.android.systemui.communal.data.repository.communalMediaRepository +import com.android.systemui.communal.data.repository.communalPrefsRepository import com.android.systemui.communal.data.repository.communalRepository import com.android.systemui.communal.data.repository.communalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.smartspace.data.repository.smartspaceRepository import org.mockito.Mockito.mock val Kosmos.communalInteractor by Kosmos.Fixture { CommunalInteractor( + applicationScope = testScope.backgroundScope, communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, + communalPrefsRepository = communalPrefsRepository, smartspaceRepository = smartspaceRepository, keyguardInteractor = keyguardInteractor, - appWidgetHost = mock(AppWidgetHost::class.java), - editWidgetsActivityStarter = mock(EditWidgetsActivityStarter::class.java), + appWidgetHost = mock(CommunalAppWidgetHost::class.java), + editWidgetsActivityStarter = editWidgetsActivityStarter, ) } + +val Kosmos.editWidgetsActivityStarter by + Kosmos.Fixture<EditWidgetsActivityStarter> { mock(EditWidgetsActivityStarter::class.java) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt new file mode 100644 index 000000000000..24670b12193a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -0,0 +1,73 @@ +/* + * 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.kosmos + +import android.content.applicationContext +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.bouncerRepository +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.classifier.falsingCollector +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.sceneContainerConfig +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.statusbar.phone.screenOffAnimationController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.util.time.systemClock + +/** Helper for using [Kosmos] from Java. */ +@Deprecated("Please convert your test to Kotlin and use [Kosmos] directly.") +class KosmosJavaAdapter( + testCase: SysuiTestCase, +) { + + private val kosmos = Kosmos() + + val testDispatcher by lazy { kosmos.testDispatcher } + val testScope by lazy { kosmos.testScope } + val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic } + val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags } + val configurationRepository by lazy { kosmos.fakeConfigurationRepository } + val configurationInteractor by lazy { kosmos.configurationInteractor } + val bouncerRepository by lazy { kosmos.bouncerRepository } + val communalRepository by lazy { kosmos.fakeCommunalRepository } + val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + val powerRepository by lazy { kosmos.fakePowerRepository } + val clock by lazy { kosmos.systemClock } + val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository } + val simBouncerInteractor by lazy { kosmos.simBouncerInteractor } + val statusBarStateController by lazy { kosmos.statusBarStateController } + val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } + val screenOffAnimationController by lazy { kosmos.screenOffAnimationController } + val fakeSceneContainerConfig by lazy { kosmos.sceneContainerConfig } + val sceneInteractor by lazy { kosmos.sceneInteractor } + val falsingCollector by lazy { kosmos.falsingCollector } + val powerInteractor by lazy { kosmos.powerInteractor } + + init { + kosmos.applicationContext = testCase.context + kosmos.testCase = testCase + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt deleted file mode 100644 index d314a25658e8..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.scene - -import android.content.Context -import android.content.applicationContext -import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor -import com.android.systemui.authentication.domain.interactor.authenticationInteractor -import com.android.systemui.bouncer.data.repository.bouncerRepository -import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor -import com.android.systemui.bouncer.domain.interactor.bouncerInteractor -import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel -import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.classifier.domain.interactor.FalsingInteractor -import com.android.systemui.classifier.domain.interactor.falsingInteractor -import com.android.systemui.classifier.falsingCollector -import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.configurationInteractor -import com.android.systemui.communal.data.repository.fakeCommunalRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.communalInteractor -import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor -import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor -import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.jank.interactionJankMonitor -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.keyguardInteractor -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testCase -import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.kosmos.testScope -import com.android.systemui.plugins.statusbar.statusBarStateController -import com.android.systemui.power.data.repository.fakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.domain.interactor.powerInteractor -import com.android.systemui.scene.data.repository.SceneContainerRepository -import com.android.systemui.scene.data.repository.sceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags -import com.android.systemui.scene.shared.model.SceneContainerConfig -import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel -import com.android.systemui.statusbar.phone.screenOffAnimationController -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository -import com.android.systemui.telephony.data.repository.fakeTelephonyRepository -import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.telephony.domain.interactor.telephonyInteractor -import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.user.domain.interactor.selectedUserInteractor -import com.android.systemui.util.time.systemClock -import kotlinx.coroutines.ExperimentalCoroutinesApi - -/** - * Utilities for creating scene container framework related repositories, interactors, and - * view-models for tests. - */ -@OptIn(ExperimentalCoroutinesApi::class) -@Deprecated("Please use Kosmos instead.") -class SceneTestUtils { - - val kosmos = Kosmos() - - constructor( - context: Context, - ) { - kosmos.applicationContext = context - } - - constructor(testCase: SysuiTestCase) : this(context = testCase.context) { - kosmos.testCase = testCase - } - - val testDispatcher by lazy { kosmos.testDispatcher } - val testScope by lazy { kosmos.testScope } - val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic } - val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags } - val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository } - val authenticationRepository by lazy { kosmos.fakeAuthenticationRepository } - val configurationRepository by lazy { kosmos.fakeConfigurationRepository } - val configurationInteractor by lazy { kosmos.configurationInteractor } - val telephonyRepository by lazy { kosmos.fakeTelephonyRepository } - val bouncerRepository by lazy { kosmos.bouncerRepository } - val communalRepository by lazy { kosmos.fakeCommunalRepository } - val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } - val powerRepository by lazy { kosmos.fakePowerRepository } - val simBouncerRepository by lazy { kosmos.fakeSimBouncerRepository } - val clock by lazy { kosmos.systemClock } - val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository } - val simBouncerInteractor by lazy { kosmos.simBouncerInteractor } - val statusBarStateController by lazy { kosmos.statusBarStateController } - val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } - val screenOffAnimationController by lazy { kosmos.screenOffAnimationController } - - fun fakeSceneContainerRepository(): SceneContainerRepository { - return kosmos.sceneContainerRepository - } - - fun fakeSceneKeys(): List<SceneKey> { - return kosmos.sceneKeys - } - - fun fakeSceneContainerConfig(): SceneContainerConfig { - return kosmos.sceneContainerConfig - } - - fun sceneInteractor(): SceneInteractor { - return kosmos.sceneInteractor - } - - fun deviceEntryInteractor(): DeviceEntryInteractor { - return kosmos.deviceEntryInteractor - } - - fun authenticationInteractor(): AuthenticationInteractor { - return kosmos.authenticationInteractor - } - - fun keyguardInteractor(): KeyguardInteractor { - return kosmos.keyguardInteractor - } - - fun communalInteractor(): CommunalInteractor { - return kosmos.communalInteractor - } - - fun bouncerInteractor(): BouncerInteractor { - return kosmos.bouncerInteractor - } - - fun notificationsPlaceholderViewModel(): NotificationsPlaceholderViewModel { - return kosmos.notificationsPlaceholderViewModel - } - - fun bouncerViewModel(): BouncerViewModel { - return kosmos.bouncerViewModel - } - - fun telephonyInteractor(): TelephonyInteractor { - return kosmos.telephonyInteractor - } - - fun falsingInteractor(): FalsingInteractor { - return kosmos.falsingInteractor - } - - fun falsingCollector(): FalsingCollector { - return kosmos.falsingCollector - } - - fun powerInteractor(): PowerInteractor { - return kosmos.powerInteractor - } - - fun selectedUserInteractor(): SelectedUserInteractor { - return kosmos.selectedUserInteractor - } - - fun bouncerActionButtonInteractor(): BouncerActionButtonInteractor { - return kosmos.bouncerActionButtonInteractor - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index 0dbade76979c..d7e948eefc95 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -20,11 +20,13 @@ import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.shared.flag.sceneContainerFlags +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.notificationsPlaceholderViewModel by Fixture { NotificationsPlaceholderViewModel( interactor = notificationStackAppearanceInteractor, + shadeInteractor = shadeInteractor, flags = sceneContainerFlags, featureFlags = featureFlagsClassic, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt index d9beabb11ad9..d80ee758269f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.policy.headsUpManager val Kosmos.windowRootViewVisibilityInteractor by Fixture { @@ -32,5 +33,6 @@ val Kosmos.windowRootViewVisibilityInteractor by Fixture { keyguardRepository = keyguardRepository, headsUpManager = headsUpManager, powerInteractor = powerInteractor, + activeNotificationsInteractor = activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt index 2bca2722be6c..24b22f870776 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/util/ScopedUnfoldTransitionProgressProvider.kt @@ -17,6 +17,7 @@ package com.android.systemui.unfold.util import android.os.Handler import android.os.Looper +import androidx.annotation.GuardedBy import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import java.util.concurrent.CopyOnWriteArrayList @@ -41,8 +42,12 @@ constructor(source: UnfoldTransitionProgressProvider? = null) : private val listeners = CopyOnWriteArrayList<TransitionProgressListener>() - @Volatile private var isReadyToHandleTransition = false - @Volatile private var isTransitionRunning = false + private val lock = Object() + + @GuardedBy("lock") private var isReadyToHandleTransition = false + // Accessed only from progress thread + private var isTransitionRunning = false + // Accessed only from progress thread private var lastTransitionProgress = PROGRESS_UNSET init { @@ -72,23 +77,44 @@ constructor(source: UnfoldTransitionProgressProvider? = null) : * the transition progress events. * * Call it with readyToHandleTransition = false when listeners can't process the events. + * + * Note that this could be called by any thread. */ fun setReadyToHandleTransition(isReadyToHandleTransition: Boolean) { - val progressHandler = this.progressHandler - if (isTransitionRunning && progressHandler != null) { - progressHandler.post { - if (isReadyToHandleTransition) { - listeners.forEach { it.onTransitionStarted() } - if (lastTransitionProgress != PROGRESS_UNSET) { - listeners.forEach { it.onTransitionProgress(lastTransitionProgress) } - } - } else { - isTransitionRunning = false - listeners.forEach { it.onTransitionFinished() } + synchronized(lock) { + this.isReadyToHandleTransition = isReadyToHandleTransition + val progressHandlerLocal = this.progressHandler + if (progressHandlerLocal != null) { + ensureInHandler(progressHandlerLocal) { reportLastProgressIfNeeded() } + } + } + } + + /** Runs directly if called from the handler thread. Posts otherwise. */ + private fun ensureInHandler(handler: Handler, f: () -> Unit) { + if (handler.looper.isCurrentThread) { + f() + } else { + handler.post(f) + } + } + + private fun reportLastProgressIfNeeded() { + assertInProgressThread() + synchronized(lock) { + if (!isTransitionRunning) { + return + } + if (isReadyToHandleTransition) { + listeners.forEach { it.onTransitionStarted() } + if (lastTransitionProgress != PROGRESS_UNSET) { + listeners.forEach { it.onTransitionProgress(lastTransitionProgress) } } + } else { + isTransitionRunning = false + listeners.forEach { it.onTransitionFinished() } } } - this.isReadyToHandleTransition = isReadyToHandleTransition } override fun addCallback(listener: TransitionProgressListener) { @@ -106,34 +132,42 @@ constructor(source: UnfoldTransitionProgressProvider? = null) : override fun onTransitionStarted() { assertInProgressThread() - isTransitionRunning = true - if (isReadyToHandleTransition) { - listeners.forEach { it.onTransitionStarted() } + synchronized(lock) { + isTransitionRunning = true + if (isReadyToHandleTransition) { + listeners.forEach { it.onTransitionStarted() } + } } } override fun onTransitionProgress(progress: Float) { assertInProgressThread() - if (isReadyToHandleTransition) { - listeners.forEach { it.onTransitionProgress(progress) } + synchronized(lock) { + if (isReadyToHandleTransition) { + listeners.forEach { it.onTransitionProgress(progress) } + } + lastTransitionProgress = progress } - lastTransitionProgress = progress } override fun onTransitionFinishing() { assertInProgressThread() - if (isReadyToHandleTransition) { - listeners.forEach { it.onTransitionFinishing() } + synchronized(lock) { + if (isReadyToHandleTransition) { + listeners.forEach { it.onTransitionFinishing() } + } } } override fun onTransitionFinished() { assertInProgressThread() - if (isReadyToHandleTransition) { - listeners.forEach { it.onTransitionFinished() } + synchronized(lock) { + if (isReadyToHandleTransition) { + listeners.forEach { it.onTransitionFinished() } + } + isTransitionRunning = false + lastTransitionProgress = PROGRESS_UNSET } - isTransitionRunning = false - lastTransitionProgress = PROGRESS_UNSET } private fun assertInProgressThread() { @@ -151,7 +185,7 @@ constructor(source: UnfoldTransitionProgressProvider? = null) : } } - companion object { - private const val PROGRESS_UNSET = -1f + private companion object { + const val PROGRESS_UNSET = -1f } } diff --git a/ravenwood/junit-src/android/platform/test/annotations/ExcludeUnderRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/ExcludeUnderRavenwood.java new file mode 100644 index 000000000000..2ef381e8682c --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/annotations/ExcludeUnderRavenwood.java @@ -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 android.platform.test.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Tests marked with this annotation are excluded when running under a Ravenwood test environment. + * + * A more specific method-level annotation always takes precedence over any class-level + * annotation, and an {@link IncludeUnderRavenwood} annotation always takes precedence over + * an {@link ExcludeUnderRavenwood} annotation. + * + * This annotation only takes effect when the containing class has a {@code + * RavenwoodRule} configured. Ignoring is accomplished by throwing an {@code org.junit + * .AssumptionViolatedException} which test infrastructure treats as being ignored. + * + * This annotation has no effect on any other non-Ravenwood test environments. + * + * @hide + */ +@Inherited +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExcludeUnderRavenwood { +} diff --git a/ravenwood/junit-src/android/platform/test/annotations/IncludeUnderRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/IncludeUnderRavenwood.java new file mode 100644 index 000000000000..0b2e32f67960 --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/annotations/IncludeUnderRavenwood.java @@ -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 android.platform.test.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Tests marked with this annotation are included when running under a Ravenwood test environment. + * + * A more specific method-level annotation always takes precedence over any class-level + * annotation, and an {@link IncludeUnderRavenwood} annotation always takes precedence over + * an {@link ExcludeUnderRavenwood} annotation. + * + * This annotation only takes effect when the containing class has a {@code + * RavenwoodRule} configured. Ignoring is accomplished by throwing an {@code org.junit + * .AssumptionViolatedException} which test infrastructure treats as being ignored. + * + * This annotation has no effect on any other non-Ravenwood test environments. + * + * @hide + */ +@Inherited +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IncludeUnderRavenwood { +} diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index d96787480cfa..53da8ba14a2c 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -18,7 +18,9 @@ package android.platform.test.ravenwood; import static org.junit.Assert.fail; +import android.platform.test.annotations.ExcludeUnderRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.annotations.IncludeUnderRavenwood; import org.junit.Assume; import org.junit.rules.TestRule; @@ -109,21 +111,52 @@ public class RavenwoodRule implements TestRule { } /** - * Test if the given {@link Description} has been marked with an {@link IgnoreUnderRavenwood} - * annotation, either at the method or class level. + * Determine if the given {@link Description} should be included when running under the + * Ravenwood test environment. + * + * A more specific method-level annotation always takes precedence over any class-level + * annotation, and an {@link IncludeUnderRavenwood} annotation always takes precedence over + * an {@link ExcludeUnderRavenwood} annotation. */ - private static boolean hasIgnoreUnderRavenwoodAnnotation(Description description) { - if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) { + private boolean shouldIncludeUnderRavenwood(Description description) { + // Stopgap for http://g/ravenwood/EPAD-N5ntxM + if (description.getMethodName().endsWith("$noRavenwood")) { + return false; + } + + // First, consult any method-level annotations + if (description.getAnnotation(IncludeUnderRavenwood.class) != null) { return true; - } else if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { + } + if (description.getAnnotation(ExcludeUnderRavenwood.class) != null) { + return false; + } + if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { + return false; + } + + // Otherwise, consult any class-level annotations + if (description.getTestClass().getAnnotation(IncludeUnderRavenwood.class) != null) { return true; - } else { + } + if (description.getTestClass().getAnnotation(ExcludeUnderRavenwood.class) != null) { + return false; + } + if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) { return false; } + + // When no annotations have been requested, assume test should be included + return true; } @Override public Statement apply(Statement base, Description description) { + // No special treatment when running outside Ravenwood; run tests as-is + if (!IS_UNDER_RAVENWOOD) { + return base; + } + if (ENABLE_PROBE_IGNORED) { return applyProbeIgnored(base, description); } else { @@ -138,14 +171,7 @@ public class RavenwoodRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { - if (hasIgnoreUnderRavenwoodAnnotation(description)) { - Assume.assumeFalse(IS_UNDER_RAVENWOOD); - } - - // Stopgap for http://g/ravenwood/EPAD-N5ntxM - if (description.getMethodName().endsWith("$noRavenwood")) { - Assume.assumeFalse(IS_UNDER_RAVENWOOD); - } + Assume.assumeTrue(shouldIncludeUnderRavenwood(description)); RavenwoodRuleImpl.init(RavenwoodRule.this); try { @@ -170,19 +196,17 @@ public class RavenwoodRule implements TestRule { try { base.evaluate(); } catch (Throwable t) { - if (hasIgnoreUnderRavenwoodAnnotation(description)) { - // This failure is expected, so eat the exception and report the - // assumption failure that test authors expect - Assume.assumeFalse(IS_UNDER_RAVENWOOD); - } + // If the test isn't included, eat the exception and report the + // assumption failure that test authors expect; otherwise throw + Assume.assumeTrue(shouldIncludeUnderRavenwood(description)); throw t; } finally { RavenwoodRuleImpl.reset(RavenwoodRule.this); } - if (hasIgnoreUnderRavenwoodAnnotation(description) && IS_UNDER_RAVENWOOD) { - fail("Test was annotated with IgnoreUnderRavenwood, but it actually " - + "passed under Ravenwood; consider removing the annotation"); + if (!shouldIncludeUnderRavenwood(description)) { + fail("Test wasn't included under Ravenwood, but it actually " + + "passed under Ravenwood; consider updating annotations"); } } }; diff --git a/services/autofill/features.aconfig b/services/autofill/features.aconfig index 92e00ee0a477..727721d5319f 100644 --- a/services/autofill/features.aconfig +++ b/services/autofill/features.aconfig @@ -5,4 +5,11 @@ flag { namespace: "autofill" description: "Guards Autofill Framework against Autofill-Credman integration" bug: "296907283" +} + +flag { + name: "autofill_credman_integration_phase2" + namespace: "autofill" + description: "Guards against Autofill-Credman integration phase 2" + bug: "320730001" }
\ No newline at end of file diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java index 7fc1738f3172..123470304783 100644 --- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java +++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java @@ -108,9 +108,9 @@ final class SecondaryProviderHandler implements RemoteFillService.FillServiceCal mLastFlag = flag; if (mRemoteFillService != null && mRemoteFillService.isCredentialAutofillService()) { Slog.v(TAG, "About to call CredAutofill service as secondary provider"); - addSessionIdAndRequestIdToClientState(pendingFillRequest, + FillRequest request = addSessionIdAndRequestIdToClientState(pendingFillRequest, pendingInlineSuggestionsRequest, id); - mRemoteFillService.onFillCredentialRequest(pendingFillRequest, client); + mRemoteFillService.onFillCredentialRequest(request, client); } else { mRemoteFillService.onFillRequest(pendingFillRequest); } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index a856f424be5c..f3b74ea00a58 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1735,6 +1735,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState processResponseLockedForPcc(response, response.getClientState(), requestFlags); mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis(); + mFillResponseEventLogger.logAndEndEvent(); } @@ -1847,6 +1848,33 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } synchronized (mLock) { + // TODO(b/319913595): refactor logging for fill response for primary and secondary + // providers + // Start a new FillResponse logger for the success case. + mFillResponseEventLogger.startLogForNewResponse(); + mFillResponseEventLogger.maybeSetRequestId(fillResponse.getRequestId()); + mFillResponseEventLogger.maybeSetAppPackageUid(uid); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SUCCESS); + mFillResponseEventLogger.startResponseProcessingTime(); + // Time passed since session was created + final long fillRequestReceivedRelativeTimestamp = + SystemClock.elapsedRealtime() - mLatencyBaseTime; + mPresentationStatsEventLogger.maybeSetFillResponseReceivedTimestampMs( + (int) (fillRequestReceivedRelativeTimestamp)); + mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis( + (int) (fillRequestReceivedRelativeTimestamp)); + if (mDestroyed) { + Slog.w(TAG, "Call to Session#onSecondaryFillResponse() rejected - session: " + + id + " destroyed"); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SESSION_DESTROYED); + mFillResponseEventLogger.logAndEndEvent(); + return; + } + + List<Dataset> datasetList = fillResponse.getDatasets(); + int datasetCount = (datasetList == null) ? 0 : datasetList.size(); + mFillResponseEventLogger.maybeSetTotalDatasetsProvided(datasetCount); + mFillResponseEventLogger.maybeSetAvailableCount(datasetCount); if (mSecondaryResponses == null) { mSecondaryResponses = new SparseArray<>(2); } @@ -1859,6 +1887,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (currentView != null) { currentView.maybeCallOnFillReady(flags); } + mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis(); + mFillResponseEventLogger.logAndEndEvent(); } } diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 2fbc3cd24d65..055970819e28 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -140,8 +140,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Widget-related data handled as part of this restore operation private byte[] mWidgetData; - // Number of apps restored in this pass - private int mCount; + // Number of apps attempted to restore in this pass + private int mRestoreAttemptedAppsCount; // When did we start? private long mStartRealtime; @@ -574,7 +574,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { Slog.v(TAG, "No more packages; finishing restore"); } int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); - EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis); + EventLog.writeEvent( + EventLogTags.RESTORE_SUCCESS, mRestoreAttemptedAppsCount, millis); nextState = UnifiedRestoreState.FINAL; return; } @@ -582,7 +583,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { if (DEBUG) { Slog.i(TAG, "Next restore package: " + mRestoreDescription); } - sendOnRestorePackage(pkgName); + mRestoreAttemptedAppsCount++; + sendOnRestorePackage(mRestoreAttemptedAppsCount, pkgName); Metadata metaInfo = mPmAgent.getRestoredMetadata(pkgName); if (metaInfo == null) { @@ -810,7 +812,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // And then finally start the restore on this agent try { initiateOneRestore(mCurrentPackage, metaInfo.versionCode); - ++mCount; } catch (Exception e) { Slog.e(TAG, "Error when attempting restore: " + e.toString()); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); @@ -1331,13 +1332,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } // Tell the observer we're done - if (mObserver != null) { - try { - mObserver.restoreFinished(mStatus); - } catch (RemoteException e) { - Slog.d(TAG, "Restore observer died at restoreFinished"); - } - } + sendEndRestore(); // Clear any ongoing session timeout. backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT); @@ -1651,10 +1646,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } } - void sendOnRestorePackage(String name) { + void sendOnRestorePackage(int index, String name) { if (mObserver != null) { try { - mObserver.onUpdate(mCount, name); + mObserver.onUpdate(index, name); } catch (RemoteException e) { Slog.d(TAG, "Restore observer died in onUpdate"); mObserver = null; diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index a0301a920d96..6e906ebe887a 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -34,7 +34,7 @@ class SecureTransport extends Transport implements SecureChannel.Callback { private volatile boolean mShouldProcessRequests = false; - private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(100); + private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500); SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) { super(associationId, fd, context); diff --git a/services/core/Android.bp b/services/core/Android.bp index a6ed498e93db..fdcd27da5bdc 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -30,6 +30,14 @@ filegroup { ], } +java_library_static { + name: "services-config-update", + srcs: [ + "java/**/ConfigUpdateInstallReceiver.java", + "java/**/*.logtags", + ], +} + genrule { name: "services.core.protologsrc", srcs: [ diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 5a44ac803cb4..9d9e7c9345be 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -1387,6 +1387,8 @@ public final class BatteryService extends SystemService { case BatteryManager.BATTERY_PROPERTY_MANUFACTURING_DATE: case BatteryManager.BATTERY_PROPERTY_FIRST_USAGE_DATE: case BatteryManager.BATTERY_PROPERTY_CHARGING_POLICY: + case BatteryManager.BATTERY_PROPERTY_SERIAL_NUMBER: + case BatteryManager.BATTERY_PROPERTY_PART_STATUS: mContext.enforceCallingPermission( android.Manifest.permission.BATTERY_STATS, null); break; diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 329aac6f3a6a..9f279b1ba3fe 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -48,8 +48,6 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.am.DropboxRateLimiter; -import com.android.server.os.TombstoneProtos; -import com.android.server.os.TombstoneProtos.Tombstone; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -62,14 +60,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermissions; -import java.util.AbstractMap; import java.util.HashMap; import java.util.Iterator; -import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * Performs a number of miscellaneous, non-system-critical actions @@ -337,12 +332,12 @@ public class BootReceiver extends BroadcastReceiver { * * @param ctx Context * @param tombstone path to the tombstone - * @param tombstoneProto the parsed proto tombstone + * @param proto whether the tombstone is stored as proto * @param processName the name of the process corresponding to the tombstone * @param tmpFileLock the lock for reading/writing tmp files */ public static void addTombstoneToDropBox( - Context ctx, File tombstone, Tombstone tombstoneProto, String processName, + Context ctx, File tombstone, boolean proto, String processName, ReentrantLock tmpFileLock) { final DropBoxManager db = ctx.getSystemService(DropBoxManager.class); if (db == null) { @@ -352,33 +347,31 @@ public class BootReceiver extends BroadcastReceiver { // Check if we should rate limit and abort early if needed. DropboxRateLimiter.RateLimitResult rateLimitResult = - sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName); + sDropboxRateLimiter.shouldRateLimit( + proto ? TAG_TOMBSTONE_PROTO_WITH_HEADERS : TAG_TOMBSTONE, processName); if (rateLimitResult.shouldRateLimit()) return; HashMap<String, Long> timestamps = readTimestamps(); try { - // Remove the memory data from the proto. - Tombstone tombstoneProtoWithoutMemory = removeMemoryFromTombstone(tombstoneProto); - - final byte[] tombstoneBytes = tombstoneProtoWithoutMemory.toByteArray(); - - // Use JNI to call the c++ proto to text converter and add the headers to the tombstone. - String tombstoneWithoutMemory = new StringBuilder(getBootHeadersToLogAndUpdate()) - .append(rateLimitResult.createHeader()) - .append(getTombstoneText(tombstoneBytes)) - .toString(); - - // Add the tombstone without memory data to dropbox. - db.addText(TAG_TOMBSTONE, tombstoneWithoutMemory); - - // Add the tombstone proto to dropbox. - if (recordFileTimestamp(tombstone, timestamps)) { - tmpFileLock.lock(); - try { - addAugmentedProtoToDropbox(tombstone, tombstoneBytes, db, rateLimitResult); - } finally { - tmpFileLock.unlock(); + if (proto) { + if (recordFileTimestamp(tombstone, timestamps)) { + // We need to attach the count indicating the number of dropped dropbox entries + // due to rate limiting. Do this by enclosing the proto tombsstone in a + // container proto that has the dropped entry count and the proto tombstone as + // bytes (to avoid the complexity of reading and writing nested protos). + tmpFileLock.lock(); + try { + addAugmentedProtoToDropbox(tombstone, db, rateLimitResult); + } finally { + tmpFileLock.unlock(); + } } + } else { + // Add the header indicating how many events have been dropped due to rate limiting. + final String headers = getBootHeadersToLogAndUpdate() + + rateLimitResult.createHeader(); + addFileToDropBox(db, timestamps, headers, tombstone.getPath(), LOG_SIZE, + TAG_TOMBSTONE); } } catch (IOException e) { Slog.e(TAG, "Can't log tombstone", e); @@ -387,8 +380,11 @@ public class BootReceiver extends BroadcastReceiver { } private static void addAugmentedProtoToDropbox( - File tombstone, byte[] tombstoneBytes, DropBoxManager db, + File tombstone, DropBoxManager db, DropboxRateLimiter.RateLimitResult rateLimitResult) throws IOException { + // Read the proto tombstone file as bytes. + final byte[] tombstoneBytes = Files.readAllBytes(tombstone.toPath()); + final File tombstoneProtoWithHeaders = File.createTempFile( tombstone.getName(), ".tmp", TOMBSTONE_TMP_DIR); Files.setPosixFilePermissions( @@ -421,8 +417,6 @@ public class BootReceiver extends BroadcastReceiver { } } - private static native String getTombstoneText(byte[] tombstoneBytes); - private static void addLastkToDropBox( DropBoxManager db, HashMap<String, Long> timestamps, String headers, String footers, String filename, int maxSize, @@ -440,31 +434,6 @@ public class BootReceiver extends BroadcastReceiver { addFileWithFootersToDropBox(db, timestamps, headers, footers, filename, maxSize, tag); } - /** Removes memory information from the Tombstone proto. */ - @VisibleForTesting - public static Tombstone removeMemoryFromTombstone(Tombstone tombstoneProto) { - Tombstone.Builder tombstoneBuilder = tombstoneProto.toBuilder() - .clearMemoryMappings() - .clearThreads() - .putAllThreads(tombstoneProto.getThreadsMap().entrySet() - .stream() - .map(BootReceiver::clearMemoryDump) - .collect(Collectors.toMap(e->e.getKey(), e->e.getValue()))); - - if (tombstoneProto.hasSignalInfo()) { - tombstoneBuilder.setSignalInfo( - tombstoneProto.getSignalInfo().toBuilder().clearFaultAdjacentMetadata()); - } - - return tombstoneBuilder.build(); - } - - private static AbstractMap.SimpleEntry<Integer, TombstoneProtos.Thread> clearMemoryDump( - Map.Entry<Integer, TombstoneProtos.Thread> e) { - return new AbstractMap.SimpleEntry<Integer, TombstoneProtos.Thread>( - e.getKey(), e.getValue().toBuilder().clearMemoryDump().build()); - } - private static void addFileToDropBox( DropBoxManager db, HashMap<String, Long> timestamps, String headers, String filename, int maxSize, String tag) throws IOException { diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index 70bd4b328b43..c3916422159e 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -34,11 +34,11 @@ import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import com.android.server.wm.WindowManagerInternal; -import java.util.Collections; import java.util.Set; /** @@ -54,6 +54,10 @@ public final class SensitiveContentProtectionManagerService extends SystemServic private @Nullable MediaProjectionManager mProjectionManager; private @Nullable WindowManagerInternal mWindowManager; + final Object mSensitiveContentProtectionLock = new Object(); + @GuardedBy("mSensitiveContentProtectionLock") + private boolean mProjectionActive = false; + private final MediaProjectionManager.Callback mProjectionCallback = new MediaProjectionManager.Callback() { @Override @@ -132,14 +136,23 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } private void onProjectionStart() { - StatusBarNotification[] notifications; - try { - notifications = mNotificationListener.getActiveNotifications(); - } catch (SecurityException e) { - Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e); - notifications = new StatusBarNotification[0]; + synchronized (mSensitiveContentProtectionLock) { + mProjectionActive = true; + updateAppsThatShouldBlockScreenCapture(); } + } + + private void onProjectionEnd() { + synchronized (mSensitiveContentProtectionLock) { + mProjectionActive = false; + + // notify windowmanager to clear any sensitive notifications observed during projection + // session + mWindowManager.clearBlockedApps(); + } + } + private void updateAppsThatShouldBlockScreenCapture() { RankingMap rankingMap; try { rankingMap = mNotificationListener.getCurrentRanking(); @@ -148,41 +161,98 @@ public final class SensitiveContentProtectionManagerService extends SystemServic rankingMap = null; } - // notify windowmanager of any currently posted sensitive content notifications - Set<PackageInfo> packageInfos = getSensitivePackagesFromNotifications( - notifications, - rankingMap); - - mWindowManager.setShouldBlockScreenCaptureForApp(packageInfos); + updateAppsThatShouldBlockScreenCapture(rankingMap); } - private void onProjectionEnd() { - // notify windowmanager to clear any sensitive notifications observed during projection - // session - mWindowManager.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + private void updateAppsThatShouldBlockScreenCapture(RankingMap rankingMap) { + StatusBarNotification[] notifications; + try { + notifications = mNotificationListener.getActiveNotifications(); + } catch (SecurityException e) { + Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e); + notifications = new StatusBarNotification[0]; + } + + // notify windowmanager of any currently posted sensitive content notifications + ArraySet<PackageInfo> packageInfos = getSensitivePackagesFromNotifications( + notifications, rankingMap); + + mWindowManager.addBlockScreenCaptureForApps(packageInfos); } - private Set<PackageInfo> getSensitivePackagesFromNotifications( - StatusBarNotification[] notifications, RankingMap rankingMap) { + private ArraySet<PackageInfo> getSensitivePackagesFromNotifications( + @NonNull StatusBarNotification[] notifications, RankingMap rankingMap) { + ArraySet<PackageInfo> sensitivePackages = new ArraySet<>(); if (rankingMap == null) { Log.w(TAG, "Ranking map not initialized."); - return Collections.emptySet(); + return sensitivePackages; } - Set<PackageInfo> sensitivePackages = new ArraySet<>(); for (StatusBarNotification sbn : notifications) { - NotificationListenerService.Ranking ranking = - rankingMap.getRawRankingObject(sbn.getKey()); - if (ranking != null && ranking.hasSensitiveContent()) { - PackageInfo info = new PackageInfo(sbn.getPackageName(), sbn.getUid()); + PackageInfo info = getSensitivePackageFromNotification(sbn, rankingMap); + if (info != null) { sensitivePackages.add(info); } } return sensitivePackages; } - // TODO(b/317251408): add trigger that updates on onNotificationPosted, - // onNotificationRankingUpdate and onListenerConnected + private PackageInfo getSensitivePackageFromNotification(StatusBarNotification sbn, + RankingMap rankingMap) { + if (sbn == null) { + Log.w(TAG, "Unable to protect null notification"); + return null; + } + if (rankingMap == null) { + Log.w(TAG, "Ranking map not initialized."); + return null; + } + + NotificationListenerService.Ranking ranking = rankingMap.getRawRankingObject(sbn.getKey()); + if (ranking != null && ranking.hasSensitiveContent()) { + return new PackageInfo(sbn.getPackageName(), sbn.getUid()); + } + return null; + } + @VisibleForTesting - static class NotificationListener extends NotificationListenerService {} + class NotificationListener extends NotificationListenerService { + @Override + public void onListenerConnected() { + super.onListenerConnected(); + // Projection started before notification listener was connected + synchronized (mSensitiveContentProtectionLock) { + if (mProjectionActive) { + updateAppsThatShouldBlockScreenCapture(); + } + } + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { + super.onNotificationPosted(sbn, rankingMap); + synchronized (mSensitiveContentProtectionLock) { + if (!mProjectionActive) { + return; + } + + // notify windowmanager of any currently posted sensitive content notifications + PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap); + + if (packageInfo != null) { + mWindowManager.addBlockScreenCaptureForApps(new ArraySet(Set.of(packageInfo))); + } + } + } + + @Override + public void onNotificationRankingUpdate(RankingMap rankingMap) { + super.onNotificationRankingUpdate(rankingMap); + synchronized (mSensitiveContentProtectionLock) { + if (mProjectionActive) { + updateAppsThatShouldBlockScreenCapture(rankingMap); + } + } + } + } } diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 2b35231a51f8..ea1b0f5f66f7 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1076,6 +1076,7 @@ class StorageManagerService extends IStorageManager.Stub final UserManager userManager = mContext.getSystemService(UserManager.class); final List<UserInfo> users = userManager.getUsers(); + extendWatchdogTimeout("#onReset might be slow"); mStorageSessionController.onReset(mVold, () -> { mHandler.removeCallbacksAndMessages(null); }); diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index eb6fdd72f2c3..bd67cf42014a 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -418,6 +418,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { LinkCapacityEstimate.INVALID, LinkCapacityEstimate.INVALID))); private List<List<LinkCapacityEstimate>> mLinkCapacityEstimateLists; + private int[] mSimultaneousCellularCallingSubIds = {}; + private int[] mECBMReason; private boolean[] mECBMStarted; private int[] mSCBMReason; @@ -564,7 +566,9 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { || events.contains(TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED) || events.contains(TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED) || events.contains(TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED) - || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED); + || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED) + || events.contains(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); } private static final int MSG_USER_SWITCHED = 1; @@ -1122,6 +1126,21 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { return; } + int phoneId = -1; + int subscriptionId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; + if(Flags.preventSystemServerAndPhoneDeadlock()) { + // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, + // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + if (DBG) { + log("invalid subscription id, use default id"); + } + } else { //APP specify subID + subscriptionId = subId; + } + phoneId = getPhoneIdFromSubId(subscriptionId); + } + synchronized (mRecords) { // register IBinder b = callback.asBinder(); @@ -1141,17 +1160,23 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { r.renounceFineLocationAccess = renounceFineLocationAccess; r.callerUid = Binder.getCallingUid(); r.callerPid = Binder.getCallingPid(); - // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, - // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - if (DBG) { - log("invalid subscription id, use default id"); + + if(!Flags.preventSystemServerAndPhoneDeadlock()) { + // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, + // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + if (DBG) { + log("invalid subscription id, use default id"); + } + r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; + } else {//APP specify subID + r.subId = subId; } - r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; - } else {//APP specify subID - r.subId = subId; + r.phoneId = getPhoneIdFromSubId(r.subId); + } else { + r.subId = subscriptionId; + r.phoneId = phoneId; } - r.phoneId = getPhoneIdFromSubId(r.subId); r.eventList = events; if (DBG) { @@ -1427,6 +1452,15 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } + if (events.contains(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) { + try { + r.callback.onSimultaneousCallingStateChanged( + mSimultaneousCellularCallingSubIds); + } catch (RemoteException ex) { + remove(r.binder); + } + } if (events.contains( TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)) { try { @@ -1880,8 +1914,14 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } private void notifyCarrierNetworkChangeWithPermission(int subId, boolean active) { + int phoneId = -1; + if(Flags.preventSystemServerAndPhoneDeadlock()) { + phoneId = getPhoneIdFromSubId(subId); + } synchronized (mRecords) { - int phoneId = getPhoneIdFromSubId(subId); + if(!Flags.preventSystemServerAndPhoneDeadlock()) { + phoneId = getPhoneIdFromSubId(subId); + } mCarrierNetworkChangeState[phoneId] = active; if (VDBG) { @@ -3092,6 +3132,43 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } + /** + * Notify the listeners that simultaneous cellular calling subscriptions have changed + * @param subIds The set of subIds that support simultaneous cellular calling + */ + public void notifySimultaneousCellularCallingSubscriptionsChanged(int[] subIds) { + if (!checkNotifyPermission("notifySimultaneousCellularCallingSubscriptionsChanged()")) { + return; + } + + if (VDBG) { + StringBuilder b = new StringBuilder(); + b.append("notifySimultaneousCellularCallingSubscriptionsChanged: "); + b.append("subIds = {"); + for (int i : subIds) { + b.append(" "); + b.append(i); + } + b.append("}"); + log(b.toString()); + } + + synchronized (mRecords) { + mSimultaneousCellularCallingSubIds = subIds; + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) { + try { + r.callback.onSimultaneousCallingStateChanged(subIds); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } + } + } + handleRemoveListLocked(); + } + } + @Override public void addCarrierPrivilegesCallback( int phoneId, diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 66a10e4a3c11..cd8be338a031 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -16,8 +16,10 @@ package com.android.server; +import static android.app.Flags.modesApi; import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE; import static android.app.UiModeManager.DEFAULT_PRIORITY; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; @@ -50,6 +52,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.StatusBarManager; import android.app.UiModeManager; +import android.app.UiModeManager.AttentionModeThemeOverlayType; import android.app.UiModeManager.NightModeCustomReturnType; import android.app.UiModeManager.NightModeCustomType; import android.content.BroadcastReceiver; @@ -134,6 +137,7 @@ final class UiModeManagerService extends SystemService { private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mNightMode = UiModeManager.MODE_NIGHT_NO; private int mNightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; + private int mAttentionModeThemeOverlay = UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; private final LocalTime DEFAULT_CUSTOM_NIGHT_START_TIME = LocalTime.of(22, 0); private final LocalTime DEFAULT_CUSTOM_NIGHT_END_TIME = LocalTime.of(6, 0); private LocalTime mCustomAutoNightModeStartMilliseconds = DEFAULT_CUSTOM_NIGHT_START_TIME; @@ -839,6 +843,8 @@ final class UiModeManagerService extends SystemService { ? customModeType : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; mNightMode = mode; + //deactivates AttentionMode if user toggles DarkTheme + mAttentionModeThemeOverlay = MODE_ATTENTION_THEME_OVERLAY_OFF; resetNightModeOverrideLocked(); persistNightMode(user); // on screen off will update configuration instead @@ -879,6 +885,29 @@ final class UiModeManagerService extends SystemService { } } + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + @Override + public void setAttentionModeThemeOverlay( + @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) { + setAttentionModeThemeOverlay_enforcePermission(); + + synchronized (mLock) { + if (mAttentionModeThemeOverlay != attentionModeThemeOverlayType) { + mAttentionModeThemeOverlay = attentionModeThemeOverlayType; + Binder.withCleanCallingIdentity(()-> updateLocked(0, 0)); + } + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + @Override + public @AttentionModeThemeOverlayType int getAttentionModeThemeOverlay() { + getAttentionModeThemeOverlay_enforcePermission(); + synchronized (mLock) { + return mAttentionModeThemeOverlay; + } + } + @Override public void setApplicationNightMode(@UiModeManager.NightMode int mode) { switch (mode) { @@ -1406,7 +1435,7 @@ final class UiModeManagerService extends SystemService { pw.print(Shell.nightModeToStr(mNightMode, mNightModeCustomType)); pw.print(") "); pw.print(" mOverrideOn/Off="); pw.print(mOverrideNightModeOn); pw.print("/"); pw.print(mOverrideNightModeOff); - + pw.print(" mAttentionModeThemeOverlay="); pw.print(mAttentionModeThemeOverlay); pw.print(" mNightModeLocked="); pw.println(mNightModeLocked); pw.print(" mCarModeEnabled="); pw.print(mCarModeEnabled); @@ -1685,7 +1714,7 @@ final class UiModeManagerService extends SystemService { } @UiModeManager.NightMode - private int getComputedUiModeConfiguration(@UiModeManager.NightMode int uiMode) { + private int getComputedUiModeConfiguration(int uiMode) { uiMode |= mComputedNightMode ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO; uiMode &= mComputedNightMode ? ~Configuration.UI_MODE_NIGHT_NO @@ -1980,18 +2009,26 @@ final class UiModeManagerService extends SystemService { } private void updateComputedNightModeLocked(boolean activate) { - mComputedNightMode = activate; - if (mNightMode == MODE_NIGHT_YES || mNightMode == UiModeManager.MODE_NIGHT_NO) { - return; - } - if (mOverrideNightModeOn && !mComputedNightMode) { - mComputedNightMode = true; - return; - } - if (mOverrideNightModeOff && mComputedNightMode) { - mComputedNightMode = false; - return; + boolean newComputedValue = activate; + if (mNightMode != MODE_NIGHT_YES && mNightMode != UiModeManager.MODE_NIGHT_NO) { + if (mOverrideNightModeOn && !newComputedValue) { + newComputedValue = true; + } else if (mOverrideNightModeOff && newComputedValue) { + newComputedValue = false; + } + } + + if (modesApi()) { + // Computes final night mode values based on Attention Mode. + mComputedNightMode = switch (mAttentionModeThemeOverlay) { + case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true; + case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false; + default -> newComputedValue; // case OFF + }; + } else { + mComputedNightMode = newComputedValue; } + if (mNightMode != MODE_NIGHT_AUTO || (mTwilightManager != null && mTwilightManager.getLastTwilightState() != null)) { resetNightModeOverrideLocked(); diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index fd17261bda41..c18bacb51671 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -172,6 +172,7 @@ public class Watchdog implements Dumpable { public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] { "android.hardware.audio.core.IModule/", "android.hardware.audio.core.IConfig/", + "android.hardware.audio.effect.IFactory/", "android.hardware.biometrics.face.IFace/", "android.hardware.biometrics.fingerprint.IFingerprint/", "android.hardware.bluetooth.IBluetoothHci/", diff --git a/services/core/java/com/android/server/ZramWriteback.java b/services/core/java/com/android/server/ZramWriteback.java index 5d97def129a4..39ca29eaad43 100644 --- a/services/core/java/com/android/server/ZramWriteback.java +++ b/services/core/java/com/android/server/ZramWriteback.java @@ -177,7 +177,6 @@ public final class ZramWriteback extends JobService { // back at later point if they remain untouched. js.schedule(new JobInfo.Builder(MARK_IDLE_JOB_ID, sZramWriteback) .setMinimumLatency(TimeUnit.MINUTES.toMillis(markIdleDelay)) - .setOverrideDeadline(TimeUnit.MINUTES.toMillis(markIdleDelay)) .build()); // Schedule a one time job to flush idle pages to disk. diff --git a/services/core/java/com/android/server/adaptiveauth/OWNERS b/services/core/java/com/android/server/adaptiveauth/OWNERS new file mode 100644 index 000000000000..b18810564d88 --- /dev/null +++ b/services/core/java/com/android/server/adaptiveauth/OWNERS @@ -0,0 +1 @@ +hainingc@google.com
\ No newline at end of file diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 979449ffc179..9b1fade198fc 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -5400,13 +5400,13 @@ public final class ActiveServices { return msg; } mAm.mProcessList.getAppStartInfoTracker().handleProcessServiceStart(startTimeNs, app, r, - hostingRecord, true); + true); if (isolated) { r.isolationHostProc = app; } } else { mAm.mProcessList.getAppStartInfoTracker().handleProcessServiceStart(startTimeNs, app, r, - hostingRecord, false); + false); } if (r.fgRequired) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index f6d954a69044..c9bd0b4c4648 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -7823,12 +7823,7 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void registerUidObserver(IUidObserver observer, int which, int cutpoint, String callingPackage) { - if (!hasUsageStatsPermission(callingPackage)) { - enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, - "registerUidObserver"); - } - mUidObserverController.register(observer, which, cutpoint, callingPackage, - Binder.getCallingUid(), /*uids*/null); + registerUidObserverForUids(observer, which, cutpoint, callingPackage, null /* uids */); } /** diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index b90e5cd11216..c85723525aa1 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -81,18 +81,18 @@ public final class AppStartInfoTracker { private static final int FOREACH_ACTION_REMOVE_ITEM = 1; private static final int FOREACH_ACTION_STOP_ITERATION = 2; - private static final int APP_START_INFO_HISTORY_LIST_SIZE = 16; + @VisibleForTesting static final int APP_START_INFO_HISTORY_LIST_SIZE = 16; @VisibleForTesting static final String APP_START_STORE_DIR = "procstartstore"; @VisibleForTesting static final String APP_START_INFO_FILE = "procstartinfo"; - private final Object mLock = new Object(); + @VisibleForTesting final Object mLock = new Object(); - private boolean mEnabled = false; + @VisibleForTesting boolean mEnabled = false; /** Initialized in {@link #init} and read-only after that. */ - private ActivityManagerService mService; + @VisibleForTesting ActivityManagerService mService; /** Initialized in {@link #init} and read-only after that. */ private Handler mHandler; @@ -112,7 +112,7 @@ public final class AppStartInfoTracker { * * <p>Initialized in {@link #init} and read-only after that. No lock is needed. */ - private int mAppStartInfoHistoryListSize; + @VisibleForTesting int mAppStartInfoHistoryListSize; @GuardedBy("mLock") private final ProcessMap<AppStartInfoContainer> mData; @@ -146,7 +146,8 @@ public final class AppStartInfoTracker { * Key is timestamp of launch from {@link #ActivityMetricsLaunchObserver}. */ @GuardedBy("mLock") - private ArrayMap<Long, ApplicationStartInfo> mInProgRecords = new ArrayMap<>(); + @VisibleForTesting + final ArrayMap<Long, ApplicationStartInfo> mInProgRecords = new ArrayMap<>(); AppStartInfoTracker() { mCallbacks = new SparseArray<>(); @@ -229,7 +230,7 @@ public final class AppStartInfoTracker { ApplicationStartInfo info = mInProgRecords.get(id); info.setStartType((int) temperature); addBaseFieldsFromProcessRecord(info, app); - addStartInfoLocked(info); + mInProgRecords.put(id, addStartInfoLocked(info)); } else { mInProgRecords.remove(id); } @@ -262,6 +263,7 @@ public final class AppStartInfoTracker { ApplicationStartInfo info = mInProgRecords.get(id); info.setStartupState(ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN); info.setLaunchMode(launchMode); + checkCompletenessAndCallback(info); } } @@ -281,7 +283,7 @@ public final class AppStartInfoTracker { } public void handleProcessServiceStart(long startTimeNs, ProcessRecord app, - ServiceRecord serviceRecord, HostingRecord hostingRecord, boolean cold) { + ServiceRecord serviceRecord, boolean cold) { synchronized (mLock) { if (!mEnabled) { return; @@ -297,7 +299,9 @@ public final class AppStartInfoTracker { && serviceRecord.permission.contains("android.permission.BIND_JOB_SERVICE") ? ApplicationStartInfo.START_REASON_JOB : ApplicationStartInfo.START_REASON_SERVICE); - start.setIntent(serviceRecord.intent.getIntent()); + if (serviceRecord.intent != null) { + start.setIntent(serviceRecord.intent.getIntent()); + } addStartInfoLocked(start); } } @@ -378,6 +382,7 @@ public final class AppStartInfoTracker { start.setPackageUid(app.info.uid); start.setDefiningUid(definingUid > 0 ? definingUid : app.info.uid); start.setProcessName(app.processName); + start.setPackageName(app.info.packageName); } void reportApplicationOnCreateTimeNanos(ProcessRecord app, long timeNs) { @@ -419,12 +424,12 @@ public final class AppStartInfoTracker { } private void addTimestampToStart(ProcessRecord app, long timeNs, int key) { - addTimestampToStart(app.processName, app.uid, timeNs, key); + addTimestampToStart(app.info.packageName, app.uid, timeNs, key); } - private void addTimestampToStart(String processName, int uid, long timeNs, int key) { + private void addTimestampToStart(String packageName, int uid, long timeNs, int key) { synchronized (mLock) { - AppStartInfoContainer container = mData.get(processName, uid); + AppStartInfoContainer container = mData.get(packageName, uid); if (container == null) { // Record was not created, discard new data. return; @@ -443,11 +448,11 @@ public final class AppStartInfoTracker { final ApplicationStartInfo info = new ApplicationStartInfo(raw); - AppStartInfoContainer container = mData.get(raw.getProcessName(), raw.getRealUid()); + AppStartInfoContainer container = mData.get(raw.getPackageName(), raw.getRealUid()); if (container == null) { container = new AppStartInfoContainer(mAppStartInfoHistoryListSize); container.mUid = info.getRealUid(); - mData.put(raw.getProcessName(), raw.getRealUid(), container); + mData.put(raw.getPackageName(), raw.getRealUid(), container); } container.addStartInfoLocked(info); @@ -486,6 +491,9 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } + if (maxNum == 0) { + maxNum = APP_START_INFO_HISTORY_LIST_SIZE; + } final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { @@ -891,6 +899,7 @@ public final class AppStartInfoTracker { mProcStartInfoFile.delete(); } mData.getMap().clear(); + mInProgRecords.clear(); } } @@ -960,6 +969,10 @@ public final class AppStartInfoTracker { /** Convenience method to obtain timestamp of beginning of start.*/ private static long getStartTimestamp(ApplicationStartInfo startInfo) { + if (startInfo.getStartupTimestamps() == null + || !startInfo.getStartupTimestamps().containsKey(START_TIMESTAMP_LAUNCH)) { + return -1; + } return startInfo.getStartupTimestamps().get(START_TIMESTAMP_LAUNCH); } @@ -997,7 +1010,6 @@ public final class AppStartInfoTracker { if (oldestIndex >= 0) { mInfos.remove(oldestIndex); } - mInfos.remove(0); } mInfos.add(info); Collections.sort(mInfos, (a, b) -> diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index b03183cb37d5..fa5dbd2543d3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2935,7 +2935,11 @@ public final class ProcessList { return true; } - private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs, + private static boolean unfreezePackageCgroup(int packageUID) { + return freezePackageCgroup(packageUID, false); + } + + private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs, int packageUID) { // Freeze all binder processes under the target UID (whose cgroup is about to be frozen). // Since we're going to kill these, we don't need to unfreze them later. @@ -2943,12 +2947,9 @@ public final class ProcessList { // processes (forks) should not be Binder users. int N = procs.size(); for (int i = 0; i < N; i++) { - final int uid = procs.get(i).first.uid; final int pid = procs.get(i).first.getPid(); int nRetries = 0; - // We only freeze the cgroup of the target package, so we do not need to freeze the - // Binder interfaces of dependant processes in other UIDs. - if (pid > 0 && uid == packageUID) { + if (pid > 0) { try { int rc; do { @@ -2962,12 +2963,19 @@ public final class ProcessList { } // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze - // despite being added to a new child cgroup. The cgroups of package dependant processes are - // not frozen, since it's possible this would freeze processes with no dependency on the - // package being killed here. + // despite being added to a child cgroup created after this call that would otherwise be + // unfrozen. freezePackageCgroup(packageUID, true); } + private static List<Pair<ProcessRecord, Boolean>> getUIDSublist( + List<Pair<ProcessRecord, Boolean>> procs, int startIdx) { + final int uid = procs.get(startIdx).first.uid; + int endIdx = startIdx + 1; + while (endIdx < procs.size() && procs.get(endIdx).first.uid == uid) ++endIdx; + return procs.subList(startIdx, endIdx); + } + @GuardedBy({"mService", "mProcLock"}) boolean killPackageProcessesLSP(String packageName, int appId, int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart, @@ -3063,25 +3071,36 @@ public final class ProcessList { } } - final int packageUID = UserHandle.getUid(userId, appId); - final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID - && appId <= Process.LAST_APPLICATION_UID; - if (doFreeze) { - freezeBinderAndPackageCgroup(procs, packageUID); + final boolean killingUserApp = appId >= Process.FIRST_APPLICATION_UID + && appId <= Process.LAST_APPLICATION_UID; + + if (killingUserApp) { + procs.sort((o1, o2) -> Integer.compare(o1.first.uid, o2.first.uid)); } - int N = procs.size(); - for (int i=0; i<N; i++) { - final Pair<ProcessRecord, Boolean> proc = procs.get(i); - removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second, - reasonCode, subReason, reason, !doFreeze /* async */); + int idx = 0; + while (idx < procs.size()) { + final List<Pair<ProcessRecord, Boolean>> uidProcs = getUIDSublist(procs, idx); + final int packageUID = uidProcs.get(0).first.uid; + + // Do not freeze for system apps or for dependencies of the targeted package, but + // make sure to freeze the targeted package for all users if called with USER_ALL. + final boolean doFreeze = killingUserApp && UserHandle.getAppId(packageUID) == appId; + + if (doFreeze) freezeBinderAndPackageCgroup(uidProcs, packageUID); + + for (Pair<ProcessRecord, Boolean> proc : uidProcs) { + removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second, + reasonCode, subReason, reason, !doFreeze /* async */); + } + killAppZygotesLocked(packageName, appId, userId, false /* force */); + + if (doFreeze) unfreezePackageCgroup(packageUID); + + idx += uidProcs.size(); } - killAppZygotesLocked(packageName, appId, userId, false /* force */); mService.updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END); - if (doFreeze) { - freezePackageCgroup(packageUID, false); - } - return N > 0; + return procs.size() > 0; } @GuardedBy("mService") diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java index 5189017f5bf0..b084cf3c3b12 100644 --- a/services/core/java/com/android/server/app/GameManagerSettings.java +++ b/services/core/java/com/android/server/app/GameManagerSettings.java @@ -251,6 +251,7 @@ public class GameManagerSettings { + type); } } + str.close(); } catch (XmlPullParserException | java.io.IOException e) { Slog.wtf(TAG, "Error reading game manager settings", e); return false; diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index df8d9e1a406c..2ed217a89397 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -65,6 +65,9 @@ import static android.app.AppOpsManager.opRestrictsRead; import static android.app.AppOpsManager.opToName; import static android.app.AppOpsManager.opToPublicName; import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; @@ -973,7 +976,29 @@ public class AppOpsService extends IAppOpsService.Stub { String pkgName = intent.getData().getEncodedSchemeSpecificPart(); int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); - if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) { + if (action.equals(ACTION_PACKAGE_ADDED) + && !intent.getBooleanExtra(EXTRA_REPLACING, false)) { + PackageInfo pi = getPackageManagerInternal().getPackageInfo(pkgName, + PackageManager.GET_PERMISSIONS, Process.myUid(), + UserHandle.getUserId(uid)); + boolean isSamplingTarget = isSamplingTarget(pi); + synchronized (AppOpsService.this) { + if (isSamplingTarget) { + mRarelyUsedPackages.add(pkgName); + } + UidState uidState = getUidStateLocked(uid, true); + if (!uidState.pkgOps.containsKey(pkgName)) { + uidState.pkgOps.put(pkgName, + new Ops(pkgName, uidState)); + } + + createSandboxUidStateIfNotExistsForAppLocked(uid); + } + } else if (action.equals(ACTION_PACKAGE_REMOVED) && !intent.hasExtra(EXTRA_REPLACING)) { + synchronized (AppOpsService.this) { + packageRemovedLocked(uid, pkgName); + } + } else if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) { AndroidPackage pkg = getPackageManagerInternal().getPackage(pkgName); if (pkg == null) { return; @@ -1052,7 +1077,9 @@ public class AppOpsService extends IAppOpsService.Stub { mHistoricalRegistry.systemReady(mContext.getContentResolver()); IntentFilter packageUpdateFilter = new IntentFilter(); + packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); packageUpdateFilter.addDataScheme("package"); mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL, @@ -1079,7 +1106,7 @@ public class AppOpsService extends IAppOpsService.Stub { String action; if (!ArrayUtils.contains(pkgsInUid, pkg)) { - action = Intent.ACTION_PACKAGE_REMOVED; + action = ACTION_PACKAGE_REMOVED; } else { action = Intent.ACTION_PACKAGE_REPLACED; } @@ -1160,44 +1187,6 @@ public class AppOpsService extends IAppOpsService.Stub { // onUserRemoved handled by #removeUser }); - - getPackageManagerInternal().getPackageList( - new PackageManagerInternal.PackageListObserver() { - @Override - public void onPackageAdded(String packageName, int appId) { - PackageInfo pi = getPackageManagerInternal().getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS, Process.myUid(), - mContext.getUserId()); - boolean isSamplingTarget = isSamplingTarget(pi); - int[] userIds = getUserManagerInternal().getUserIds(); - synchronized (AppOpsService.this) { - if (isSamplingTarget) { - mRarelyUsedPackages.add(packageName); - } - for (int i = 0; i < userIds.length; i++) { - int uid = UserHandle.getUid(userIds[i], appId); - UidState uidState = getUidStateLocked(uid, true); - if (!uidState.pkgOps.containsKey(packageName)) { - uidState.pkgOps.put(packageName, - new Ops(packageName, uidState)); - } - - createSandboxUidStateIfNotExistsForAppLocked(uid); - } - } - } - - @Override - public void onPackageRemoved(String packageName, int appId) { - int[] userIds = getUserManagerInternal().getUserIds(); - synchronized (AppOpsService.this) { - for (int i = 0; i < userIds.length; i++) { - int uid = UserHandle.getUid(userIds[i], appId); - packageRemovedLocked(uid, packageName); - } - } - } - }); } /** @@ -2893,6 +2882,10 @@ public class AppOpsService extends IAppOpsService.Stub { return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); } + if (proxyAttributionTag != null + && !isAttributionTagDefined(packageName, proxyPackageName, proxyAttributionTag)) { + proxyAttributionTag = null; + } synchronized (this) { final Ops ops = getOpsLocked(uid, packageName, attributionTag, @@ -3487,6 +3480,10 @@ public class AppOpsService extends IAppOpsService.Stub { return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); } + if (proxyAttributionTag != null + && !isAttributionTagDefined(packageName, proxyPackageName, proxyAttributionTag)) { + proxyAttributionTag = null; + } boolean isRestricted = false; int startType = START_TYPE_FAILED; @@ -4340,6 +4337,36 @@ public class AppOpsService extends IAppOpsService.Stub { return false; } + /** + * Checks to see if the attribution tag is defined in either package or proxyPackage. + * This method is intended for ProxyAttributionTag validation and returns false + * if it does not exist in either one of them. + * + * @param packageName Name of the package + * @param proxyPackageName Name of the proxy package + * @param attributionTag attribution tag to be checked + * + * @return boolean specifying if attribution tag is valid or not + */ + private boolean isAttributionTagDefined(@Nullable String packageName, + @Nullable String proxyPackageName, + @Nullable String attributionTag) { + if (packageName == null) { + return false; + } else if (attributionTag == null) { + return true; + } + PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); + if (proxyPackageName != null) { + AndroidPackage proxyPkg = pmInt.getPackage(proxyPackageName); + if (proxyPkg != null && isAttributionInPackage(proxyPkg, attributionTag)) { + return true; + } + } + AndroidPackage pkg = pmInt.getPackage(packageName); + return isAttributionInPackage(pkg, attributionTag); + } + private void logVerifyAndGetBypassFailure(int uid, @NonNull SecurityException e, @NonNull String methodName) { if (Process.isIsolated(uid)) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index f80228afa52d..99b45ec79571 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1812,22 +1812,21 @@ public class AudioDeviceBroker { "msg: MSG_L_SET_BT_ACTIVE_DEVICE " + "received with null profile proxy: " + btInfo)).printLog(TAG)); - sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, 0 /*delay*/); - return; - } - @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = - mBtHelper.getCodecWithFallback(btInfo.mDevice, - btInfo.mProfile, btInfo.mIsLeOutput, - "MSG_L_SET_BT_ACTIVE_DEVICE"); - mDeviceInventory.onSetBtActiveDevice(btInfo, codec, - (btInfo.mProfile - != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) - ? mAudioService.getBluetoothContextualVolumeStream() - : AudioSystem.STREAM_DEFAULT); - if (btInfo.mProfile == BluetoothProfile.LE_AUDIO - || btInfo.mProfile == BluetoothProfile.HEARING_AID) { - onUpdateCommunicationRouteClient(isBluetoothScoRequested(), - "setBluetoothActiveDevice"); + } else { + @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = + mBtHelper.getCodecWithFallback(btInfo.mDevice, + btInfo.mProfile, btInfo.mIsLeOutput, + "MSG_L_SET_BT_ACTIVE_DEVICE"); + mDeviceInventory.onSetBtActiveDevice(btInfo, codec, + (btInfo.mProfile + != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) + ? mAudioService.getBluetoothContextualVolumeStream() + : AudioSystem.STREAM_DEFAULT); + if (btInfo.mProfile == BluetoothProfile.LE_AUDIO + || btInfo.mProfile == BluetoothProfile.HEARING_AID) { + onUpdateCommunicationRouteClient(isBluetoothScoRequested(), + "setBluetoothActiveDevice"); + } } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index bf20ae3b516d..57b19cda7c12 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -764,7 +764,7 @@ public class AudioDeviceInventory { /** only public for mocking/spying, do not call outside of AudioService */ // @GuardedBy("mDeviceBroker.mSetModeLock") @VisibleForTesting - @GuardedBy("mDeviceBroker.mDeviceStateLock") + //@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock") public void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, int streamType) { diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index d5d8fd22314b..8fd2ee2bdc33 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -20,6 +20,7 @@ package com.android.server.biometrics; // TODO(b/141025588): Create separate internal and external permissions for AuthService. // TODO(b/141025588): Get rid of the USE_FINGERPRINT permission. +import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -304,6 +305,9 @@ public class AuthService extends SystemService { if (promptInfo.containsPrivateApiConfigurations()) { checkInternalPermission(); } + if (promptInfo.containsManageBioApiConfigurations()) { + checkManageBiometricPermission(); + } final long identity = Binder.clearCallingIdentity(); try { @@ -984,6 +988,11 @@ public class AuthService extends SystemService { "Must have USE_BIOMETRIC_INTERNAL permission"); } + private void checkManageBiometricPermission() { + getContext().enforceCallingOrSelfPermission(MANAGE_BIOMETRIC_DIALOG, + "Must have MANAGE_BIOMETRIC_DIALOG permission"); + } + private void checkPermission() { if (getContext().checkCallingOrSelfPermission(USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { diff --git a/services/core/java/com/android/server/broadcastradio/TEST_MAPPING b/services/core/java/com/android/server/broadcastradio/TEST_MAPPING new file mode 100644 index 000000000000..ee4eeb634c84 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "imports": [ + { + "path": "frameworks/base/core/tests/BroadcastRadioTests" + } + ] +} diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java index 1f59b57d2da9..c260f10b61a6 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionManagerInternal.java @@ -17,6 +17,7 @@ package com.android.server.grammaticalinflection; import android.annotation.Nullable; +import android.content.res.Configuration; /** * System-server internal interface to the {@link android.app.GrammaticalInflectionManager}. @@ -37,5 +38,14 @@ public abstract class GrammaticalInflectionManagerInternal { * at the time this is called, to be referenced later when the app is installed. */ public abstract void stageAndApplyRestoredPayload(byte[] payload, int userId); + + /** + * Get the current system grammatical gender of privileged application. + * + * @return the value of grammatical gender + * + * @see Configuration#getGrammaticalGender + */ + public abstract @Configuration.GrammaticalGender int getSystemGrammaticalGender(int userId); } diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java index 68848a2ad426..6eb7e9559b8d 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java @@ -19,9 +19,12 @@ package com.android.server.grammaticalinflection; import static android.app.Flags.systemTermsOfAddressEnabled; import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; +import static com.android.server.grammaticalinflection.GrammaticalInflectionUtils.checkSystemGrammaticalGenderPermission; + import android.annotation.Nullable; import android.app.GrammaticalInflectionManager; import android.app.IGrammaticalInflectionManager; +import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManagerInternal; import android.os.Binder; @@ -30,6 +33,7 @@ import android.os.Process; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemProperties; +import android.permission.PermissionManager; import android.util.AtomicFile; import android.util.Log; import android.util.SparseIntArray; @@ -75,6 +79,8 @@ public class GrammaticalInflectionService extends SystemService { private PackageManagerInternal mPackageManagerInternal; private GrammaticalInflectionService.GrammaticalInflectionBinderService mBinderService; + private PermissionManager mPermissionManager; + private Context mContext; /** * Initializes the system service. @@ -88,11 +94,12 @@ public class GrammaticalInflectionService extends SystemService { */ public GrammaticalInflectionService(Context context) { super(context); + mContext = context; mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); - mBackupHelper = new GrammaticalInflectionBackupHelper( - this, context.getPackageManager()); + mBackupHelper = new GrammaticalInflectionBackupHelper(this, context.getPackageManager()); mBinderService = new GrammaticalInflectionBinderService(); + mPermissionManager = context.getSystemService(PermissionManager.class); } @Override @@ -112,7 +119,7 @@ public class GrammaticalInflectionService extends SystemService { } @Override - public void setSystemWideGrammaticalGender(int userId, int grammaticalGender) { + public void setSystemWideGrammaticalGender(int grammaticalGender, int userId) { checkCallerIsSystem(); checkSystemTermsOfAddressIsEnabled(); GrammaticalInflectionService.this.setSystemWideGrammaticalGender(grammaticalGender, @@ -120,16 +127,17 @@ public class GrammaticalInflectionService extends SystemService { } @Override - public int getSystemGrammaticalGender(int userId) { + public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) { checkSystemTermsOfAddressIsEnabled(); - return GrammaticalInflectionService.this.getSystemGrammaticalGender(userId); + return GrammaticalInflectionService.this.getSystemGrammaticalGender(attributionSource, + userId); } @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - (new GrammaticalInflectionShellCommand(mBinderService)) + (new GrammaticalInflectionShellCommand(mBinderService, mContext.getAttributionSource())) .exec(this, in, out, err, args, callback, resultReceiver); } }; @@ -148,6 +156,13 @@ public class GrammaticalInflectionService extends SystemService { public void stageAndApplyRestoredPayload(byte[] payload, int userId) { mBackupHelper.stageAndApplyRestoredPayload(payload, userId); } + + @Override + public int getSystemGrammaticalGender(int userId) { + checkCallerIsSystem(); + return GrammaticalInflectionService.this.getSystemGrammaticalGender( + mContext.getAttributionSource(), userId); + } } protected int getApplicationGrammaticalGender(String appPackageName, int userId) { @@ -211,9 +226,24 @@ public class GrammaticalInflectionService extends SystemService { } } - // TODO(b/298591009): Add a new AppOp value for the apps that want to access the grammatical - // gender. - public int getSystemGrammaticalGender(int userId) { + public int getSystemGrammaticalGender(AttributionSource attributionSource, int userId) { + String packageName = attributionSource.getPackageName(); + if (packageName == null) { + Log.d(TAG, "Package name is null."); + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + + int callingUid = Binder.getCallingUid(); + if (mPackageManagerInternal.getPackageUid(packageName, 0, userId) != callingUid) { + Log.d(TAG, + "Package " + packageName + " does not belong to the calling uid " + callingUid); + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + + if (!checkSystemGrammaticalGenderPermission(mPermissionManager, attributionSource)) { + return GRAMMATICAL_GENDER_NOT_SPECIFIED; + } + synchronized (mLock) { final File file = getGrammaticalGenderFile(userId); if (!file.exists()) { diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java index d22372860ead..cdda69278b2c 100644 --- a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionShellCommand.java @@ -21,6 +21,7 @@ import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED import android.app.ActivityManager; import android.app.GrammaticalInflectionManager; import android.app.IGrammaticalInflectionManager; +import android.content.AttributionSource; import android.content.res.Configuration; import android.os.RemoteException; import android.os.ShellCommand; @@ -44,9 +45,12 @@ class GrammaticalInflectionShellCommand extends ShellCommand { } private final IGrammaticalInflectionManager mBinderService; + private AttributionSource mAttributionSource; - GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager) { + GrammaticalInflectionShellCommand(IGrammaticalInflectionManager grammaticalInflectionManager, + AttributionSource attributionSource) { mBinderService = grammaticalInflectionManager; + mAttributionSource = attributionSource; } @Override @@ -115,7 +119,7 @@ class GrammaticalInflectionShellCommand extends ShellCommand { } while (true); try { - mBinderService.setSystemWideGrammaticalGender(userId, grammaticalGender); + mBinderService.setSystemWideGrammaticalGender(grammaticalGender, userId); } catch (RemoteException e) { getOutPrintWriter().println("Remote Exception: " + e); } @@ -141,7 +145,8 @@ class GrammaticalInflectionShellCommand extends ShellCommand { } while (true); try { - int grammaticalGender = mBinderService.getSystemGrammaticalGender(userId); + int grammaticalGender = mBinderService.getSystemGrammaticalGender(mAttributionSource, + userId); getOutPrintWriter().println(GRAMMATICAL_GENDER_MAP.get(grammaticalGender)); } catch (RemoteException e) { getOutPrintWriter().println("Remote Exception: " + e); diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java new file mode 100644 index 000000000000..f056561f20e0 --- /dev/null +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionUtils.java @@ -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.server.grammaticalinflection; + +import static android.Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER; + +import android.annotation.NonNull; +import android.content.AttributionSource; +import android.permission.PermissionManager; +import android.util.Log; + +/** + * Utility methods for system grammatical gender. + */ +public class GrammaticalInflectionUtils { + + private static final String TAG = "GrammaticalInflectionUtils"; + + public static boolean checkSystemGrammaticalGenderPermission( + @NonNull PermissionManager permissionManager, + @NonNull AttributionSource attributionSource) { + int permissionCheckResult = permissionManager.checkPermissionForDataDelivery( + READ_SYSTEM_GRAMMATICAL_GENDER, + attributionSource, /* message= */ null); + if (permissionCheckResult != PermissionManager.PERMISSION_GRANTED) { + Log.v(TAG, "AttributionSource: " + attributionSource + + " does not have READ_SYSTEM_GRAMMATICAL_GENDER permission."); + return false; + } + return true; + } +} diff --git a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java index e827866368fe..10c01864457e 100755 --- a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java +++ b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java @@ -140,7 +140,13 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction { wrapUpAndFinish(); return; } - + // Check if the action was finished before the callback was called. + // See {@link HdmiCecFeatureAction#finish}. + if (mState == STATE_NONE) { + Slog.v(TAG, "Action was already finished before the callback was called."); + wrapUpAndFinish(); + return; + } Slog.v(TAG, "Device detected: " + ackedAddress); allocateDevices(ackedAddress); if (mDelayPeriod > 0) { @@ -453,7 +459,6 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction { wrapUpAndFinish(); return; } - // If finished current stage, move on to next stage. if (mProcessedDeviceCount == mDevices.size()) { mProcessedDeviceCount = 0; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java index 9087354dee40..a79fb88ea8b6 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecController.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java @@ -160,6 +160,9 @@ final class HdmiCecController { // This variable is used for testing, in order to delay the logical address allocation. private long mLogicalAddressAllocationDelay = 0; + // This variable is used for testing, in order to delay polling devices. + private long mPollDevicesDelay = 0; + // Private constructor. Use HdmiCecController.create(). private HdmiCecController( HdmiControlService service, NativeWrapper nativeWrapper, HdmiCecAtomWriter atomWriter) { @@ -463,6 +466,14 @@ final class HdmiCecController { } /** + * This method is used for testing, in order to delay polling devices. + */ + @VisibleForTesting + void setPollDevicesDelay(long delay) { + mPollDevicesDelay = delay; + } + + /** * Returns true if the language code is well-formed. */ @VisibleForTesting static boolean isLanguage(String language) { @@ -523,7 +534,10 @@ final class HdmiCecController { // Extract polling candidates. No need to poll against local devices. List<Integer> pollingCandidates = pickPollCandidates(pickStrategy); ArrayList<Integer> allocated = new ArrayList<>(); - runDevicePolling(sourceAddress, pollingCandidates, retryCount, callback, allocated); + mControlHandler.postDelayed( + () -> runDevicePolling( + sourceAddress, pollingCandidates, retryCount, callback, allocated), + mPollDevicesDelay); } private List<Integer> pickPollCandidates(int pickStrategy) { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 1cd267dee2fe..d34661d4d6ac 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -1290,15 +1290,19 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { mService.getHdmiCecNetwork().removeCecSwitches(portId); } - // Turning System Audio Mode off when the AVR is unlugged or standby. - // When the device is not unplugged but reawaken from standby, we check if the System - // Audio Control Feature is enabled or not then decide if turning SAM on/off accordingly. - if (getAvrDeviceInfo() != null && portId == getAvrDeviceInfo().getPortId()) { - HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected); - if (!connected) { - setSystemAudioMode(false); - } else { - onNewAvrAdded(getAvrDeviceInfo()); + if (!mService.isEarcEnabled() || !mService.isEarcSupported()) { + HdmiDeviceInfo avr = getAvrDeviceInfo(); + if (avr != null + && portId == avr.getPortId() + && isConnectedToArcPort(avr.getPhysicalAddress())) { + HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected); + if (connected) { + if (mArcEstablished) { + enableAudioReturnChannel(true); + } + } else { + enableAudioReturnChannel(false); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index eaf754dc7520..e0e825d9147a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -3617,7 +3617,7 @@ public class HdmiControlService extends SystemService { } } - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) protected boolean isEarcSupported() { synchronized (mLock) { return mEarcSupported; diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java index f3532e5ce7e9..b6c0e5d970a1 100644 --- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java +++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java @@ -53,6 +53,7 @@ final class NewDeviceAction extends HdmiCecFeatureAction { private int mVendorId; private String mDisplayName; private int mTimeoutRetry; + private HdmiDeviceInfo mOldDeviceInfo; /** * Constructor. @@ -73,6 +74,38 @@ final class NewDeviceAction extends HdmiCecFeatureAction { @Override public boolean start() { + mOldDeviceInfo = + localDevice().mService.getHdmiCecNetwork().getCecDeviceInfo(mDeviceLogicalAddress); + // If there's deviceInfo with same (logical address, physical address) set + // Then addCecDevice should be delayed until system information process is finished + if (mOldDeviceInfo != null + && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress) { + Slog.d(TAG, "Start NewDeviceAction with old deviceInfo:[" + + mOldDeviceInfo.toString() + "]"); + } else { + // Add the device ahead with default information to handle <Active Source> + // promptly, rather than waiting till the new device action is finished. + Slog.d(TAG, "Start NewDeviceAction with default deviceInfo"); + HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder() + .setLogicalAddress(mDeviceLogicalAddress) + .setPhysicalAddress(mDevicePhysicalAddress) + .setPortId(tv().getPortId(mDevicePhysicalAddress)) + .setDeviceType(mDeviceType) + .setVendorId(Constants.VENDOR_ID_UNKNOWN) + .build(); + // If a deviceInfo with same logical address but different physical address exists + // We should remove the old deviceInfo first + // This will happen if the interval between unplugging and plugging device is too short + // and HotplugDetection Action fails to remove the old deviceInfo, or when the newly + // plugged device violates HDMI Spec and uses an occupied logical address + if (mOldDeviceInfo != null) { + Slog.d(TAG, "Remove device by NewDeviceAction, logical address conflicts: " + + mDevicePhysicalAddress); + localDevice().mService.getHdmiCecNetwork().removeCecDevice( + localDevice(), mDeviceLogicalAddress); + } + localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo); + } requestOsdName(true); return true; } @@ -182,14 +215,30 @@ final class NewDeviceAction extends HdmiCecFeatureAction { .setVendorId(mVendorId) .setDisplayName(mDisplayName) .build(); - localDevice().mService.getHdmiCecNetwork().updateCecDevice(deviceInfo); - // Consume CEC messages we already got for this newly found device. - tv().processDelayedMessages(mDeviceLogicalAddress); + // Check if oldDevice is same as newDevice + // If so, don't add newDevice info, preventing ARC or HDMI source re-connection + if (mOldDeviceInfo != null + && mOldDeviceInfo.getLogicalAddress() == mDeviceLogicalAddress + && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress + && mOldDeviceInfo.getDeviceType() == mDeviceType + && mOldDeviceInfo.getVendorId() == mVendorId + && mOldDeviceInfo.getDisplayName().equals(mDisplayName)) { + // Consume CEC messages we already got for this newly found device. + tv().processDelayedMessages(mDeviceLogicalAddress); + Slog.d(TAG, "Ignore NewDevice, deviceInfo is same as current device"); + Slog.d(TAG, "Old:[" + mOldDeviceInfo.toString() + + "]; New:[" + deviceInfo.toString() + "]"); + } else { + Slog.d(TAG, "Add NewDevice:[" + deviceInfo.toString() + "]"); + localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo); - if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, - mDeviceLogicalAddress)) { - tv().onNewAvrAdded(deviceInfo); + // Consume CEC messages we already got for this newly found device. + tv().processDelayedMessages(mDeviceLogicalAddress); + if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, + mDeviceLogicalAddress)) { + tv().onNewAvrAdded(deviceInfo); + } } } diff --git a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java index 1153cc37e3da..8a3a56cdc9ca 100644 --- a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java +++ b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java @@ -16,6 +16,8 @@ package com.android.server.health; +import static android.os.Flags.batteryPartStatusApi; + import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.health.BatteryHealthData; @@ -150,6 +152,18 @@ class HealthServiceWrapperAidl extends HealthServiceWrapper { healthData = service.getBatteryHealthData(); prop.setLong(healthData.batteryStateOfHealth); break; + case BatteryManager.BATTERY_PROPERTY_SERIAL_NUMBER: + if (batteryPartStatusApi()) { + healthData = service.getBatteryHealthData(); + prop.setString(healthData.batterySerialNumber); + } + break; + case BatteryManager.BATTERY_PROPERTY_PART_STATUS: + if (batteryPartStatusApi()) { + healthData = service.getBatteryHealthData(); + prop.setLong(healthData.batteryPartStatus); + } + break; } } catch (UnsupportedOperationException e) { // Leave prop untouched. diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 6236e2b933bc..46668de042d4 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -1370,7 +1370,7 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { public boolean isVirtualDevice(int deviceId) { VirtualDeviceManagerInternal vdm = LocalServices.getService( VirtualDeviceManagerInternal.class); - return vdm == null || vdm.isInputDeviceOwnedByVirtualDevice(deviceId); + return vdm != null && vdm.isInputDeviceOwnedByVirtualDevice(deviceId); } private static int[] getScriptCodes(@Nullable Locale locale) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java index 295793569a25..a7632519b7bc 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java @@ -77,8 +77,6 @@ final class InputMethodInfoUtils { return this; } - // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be - // documented more clearly. InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { // If one or more auxiliary input methods are available, OK to stop populating the list. for (final InputMethodInfo imi : mInputMethodSet) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 43058080d84d..834ba20b84fd 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -168,7 +168,7 @@ final class InputMethodSubtypeSwitchingController { final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodListLocked(); if (imis.isEmpty()) { - return Collections.emptyList(); + return new ArrayList<>(); } if (isScreenLocked && includeAuxiliarySubtypes) { if (DEBUG) { diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 21e7befc1b89..0ae60360883d 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -44,6 +44,7 @@ import android.util.Log; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.media.flags.Flags; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -318,11 +319,19 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider @Override public void onBindingDied(ComponentName name) { - if (DEBUG) { - Slog.d(TAG, this + ": Service binding died"); - } unbind(); - if (shouldBind()) { + if (Flags.enablePreventionOfKeepAliveRouteProviders()) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Route provider service (%s) binding died, but we did not rebind.", + name.toString())); + } else if (shouldBind()) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Rebound to provider service (%s) after binding died.", + name.toString())); bind(); } } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java index bd252e7fdfd2..3d717a871cf1 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java @@ -33,6 +33,8 @@ import android.os.UserHandle; import android.util.Log; import android.util.Slog; +import com.android.media.flags.Flags; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -86,7 +88,9 @@ final class MediaRoute2ProviderWatcher { filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addAction(Intent.ACTION_PACKAGE_REPLACED); - filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + if (!Flags.enablePreventionOfKeepAliveRouteProviders()) { + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + } filter.addDataScheme("package"); mContext.registerReceiverAsUser(mScanPackagesReceiver, new UserHandle(mUserId), filter, null, mHandler); 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 f6571d94d554..550aed51c8e2 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -304,7 +304,7 @@ public final class MediaProjectionManagerService extends SystemService } @VisibleForTesting - void addCallback(final IMediaProjectionWatcherCallback callback) { + MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) { IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { @@ -314,6 +314,7 @@ public final class MediaProjectionManagerService extends SystemService synchronized (mLock) { mCallbackDelegate.add(callback); linkDeathRecipientLocked(callback, deathRecipient); + return mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null; } } @@ -786,11 +787,11 @@ public final class MediaProjectionManagerService extends SystemService @Override //Binder call @EnforcePermission(MANAGE_MEDIA_PROJECTION) - public void addCallback(final IMediaProjectionWatcherCallback callback) { + public MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) { addCallback_enforcePermission(); final long token = Binder.clearCallingIdentity(); try { - MediaProjectionManagerService.this.addCallback(callback); + return MediaProjectionManagerService.this.addCallback(callback); } finally { Binder.restoreCallingIdentity(token); } @@ -1244,7 +1245,7 @@ public final class MediaProjectionManagerService extends SystemService } public MediaProjectionInfo getProjectionInfo() { - return new MediaProjectionInfo(packageName, userHandle); + return new MediaProjectionInfo(packageName, userHandle, mLaunchCookie); } boolean requiresForegroundService() { diff --git a/services/core/java/com/android/server/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java index 4d19eade5a05..d7188c7f10c6 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java +++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java @@ -42,7 +42,6 @@ import android.util.ArraySet; import android.util.Log; import android.util.Slog; -import com.android.internal.annotations.Keep; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.RingBuffer; import com.android.server.am.ProcessList; @@ -414,7 +413,7 @@ public class NetworkPolicyLogger { private static final Date sDate = new Date(); public LogBuffer(int capacity) { - super(Data.class, capacity); + super(Data::new, Data[]::new, capacity); } public void uidStateChanged(int uid, int procState, long procStateSeq, @@ -690,12 +689,8 @@ public class NetworkPolicyLogger { /** * Container class for all networkpolicy events data. - * - * Note: This class needs to be public for RingBuffer class to be able to create - * new instances of this. */ - @Keep - public static final class Data { + private static final class Data { public int type; public long timeStamp; diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java index 71a6b5ed0581..ab650afe68a7 100644 --- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -16,7 +16,8 @@ package com.android.server.notification; -import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import android.app.UiModeManager; import android.app.WallpaperManager; @@ -128,10 +129,9 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { private void updateNightModeImmediately(boolean useNightMode) { Binder.withCleanCallingIdentity(() -> { - // TODO: b/314285749 - Placeholder; use real APIs when available. - mUiModeManager.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME); - mUiModeManager.setNightModeActivatedForCustomMode(MODE_NIGHT_CUSTOM_TYPE_BEDTIME, - useNightMode); + mUiModeManager.setAttentionModeThemeOverlay( + useNightMode ? MODE_ATTENTION_THEME_OVERLAY_NIGHT + : MODE_ATTENTION_THEME_OVERLAY_OFF); }); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c9f45e523e3b..2ae040a69583 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -5931,8 +5931,7 @@ public class NotificationManagerService extends SystemService { newVisualEffects, policy.priorityConversationSenders); if (shouldApplyAsImplicitRule) { - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy, - origin); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy); } else { ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index c7f570353a5b..93ffd974bb80 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -32,6 +32,7 @@ import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; +import static com.android.internal.util.Preconditions.checkArgument; import android.annotation.DrawableRes; import android.annotation.NonNull; @@ -427,6 +428,7 @@ public class ZenModeHelper { public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("addAutomaticZenRule", origin); if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) { PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner()); if (component == null) { @@ -525,6 +527,7 @@ public class ZenModeHelper { public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("updateAutomaticZenRule", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -602,7 +605,11 @@ public class ZenModeHelper { rule = newImplicitZenRule(callingPkg); newConfig.automaticRules.put(rule.id, rule); } - rule.zenMode = zenMode; + // If the user has changed the rule's *zenMode*, then don't let app overwrite it. + // We allow the update if the user has only changed other aspects of the rule. + if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) { + rule.zenMode = zenMode; + } rule.snoozing = false; rule.condition = new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_activated), @@ -625,7 +632,7 @@ public class ZenModeHelper { * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}. */ void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid, - NotificationManager.Policy policy, @ConfigChangeOrigin int origin) { + NotificationManager.Policy policy) { if (!android.app.Flags.modesApi()) { Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!"); return; @@ -641,10 +648,17 @@ public class ZenModeHelper { rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; newConfig.automaticRules.put(rule.id, rule); } - // TODO: b/308673679 - Keep user customization of this rule! - rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); - setConfigLocked(newConfig, /* triggeringComponent= */ null, origin, - "applyGlobalPolicyAsImplicitZenRule", callingUid); + // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it. + // We allow the update if the user has only changed other aspects of the rule. + if (rule.zenPolicyUserModifiedFields == 0) { + updatePolicy( + rule, + ZenAdapters.notificationPolicyToZenPolicy(policy), + /* updateBitmask= */ false); + + setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, + "applyGlobalPolicyAsImplicitZenRule", callingUid); + } } } @@ -726,6 +740,7 @@ public class ZenModeHelper { boolean removeAutomaticZenRule(String id, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("removeAutomaticZenRule", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -758,6 +773,7 @@ public class ZenModeHelper { boolean removeAutomaticZenRules(String packageName, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("removeAutomaticZenRules", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -806,6 +822,7 @@ public class ZenModeHelper { void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin, int callingUid) { + requirePublicOrigin("setAutomaticZenRuleState", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return; @@ -819,6 +836,7 @@ public class ZenModeHelper { void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition, @ConfigChangeOrigin int origin, int callingUid) { + requirePublicOrigin("setAutomaticZenRuleState", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return; @@ -988,7 +1006,7 @@ public class ZenModeHelper { return null; } - void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, + private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, @ConfigChangeOrigin int origin, boolean isNew) { if (Flags.modesApi()) { // These values can always be edited by the app, so we apply changes immediately. @@ -1053,11 +1071,9 @@ public class ZenModeHelper { rule.zenMode = newZenMode; // Updates the bitmask and values for all policy fields, based on the origin. - rule.zenPolicy = updatePolicy(rule.zenPolicy, automaticZenRule.getZenPolicy(), - updateBitmask); + updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask); // Updates the bitmask and values for all device effect fields, based on the origin. - rule.zenDeviceEffects = updateZenDeviceEffects( - rule.zenDeviceEffects, automaticZenRule.getDeviceEffects(), + updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(), origin == UPDATE_ORIGIN_APP, updateBitmask); } else { if (rule.enabled != automaticZenRule.isEnabled()) { @@ -1069,13 +1085,6 @@ public class ZenModeHelper { rule.enabled = automaticZenRule.isEnabled(); rule.modified = automaticZenRule.isModified(); rule.zenPolicy = automaticZenRule.getZenPolicy(); - if (Flags.modesApi()) { - rule.zenDeviceEffects = updateZenDeviceEffects( - rule.zenDeviceEffects, - automaticZenRule.getDeviceEffects(), - origin == UPDATE_ORIGIN_APP, - origin == UPDATE_ORIGIN_USER); - } rule.zenMode = NotificationManager.zenModeFromInterruptionFilter( automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF); rule.configurationActivity = automaticZenRule.getConfigurationActivity(); @@ -1099,28 +1108,28 @@ public class ZenModeHelper { } /** - * Modifies {@link ZenPolicy} that is being stored as part of a new or updated ZenRule. - * Returns a policy based on {@code oldPolicy}, but with fields updated to match - * {@code newPolicy} where they differ, and updating the internal user-modified bitmask to - * track these changes, if applicable based on {@code origin}. + * Modifies the {@link ZenPolicy} associated to a new or updated ZenRule. + * + * <p>The new policy is {@code newPolicy}, while the user-modified bitmask is updated to reflect + * the changes being applied (if applicable, i.e. if the update is from the user). */ - @Nullable - private ZenPolicy updatePolicy(@Nullable ZenPolicy oldPolicy, @Nullable ZenPolicy newPolicy, - boolean updateBitmask) { - // If the update is to make the policy null, we don't need to update the bitmask, - // because it won't be stored anywhere anyway. + private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy, + boolean updateBitmask) { if (newPolicy == null) { - return null; + // TODO: b/319242206 - Treat as newPolicy == default policy and continue below. + zenRule.zenPolicy = null; + return; } // If oldPolicy is null, we compare against the default policy when determining which // fields in the bitmask should be marked as updated. - if (oldPolicy == null) { - oldPolicy = mDefaultConfig.toZenPolicy(); - } + ZenPolicy oldPolicy = + zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy(); + + zenRule.zenPolicy = newPolicy; - int userModifiedFields = oldPolicy.getUserModifiedFields(); if (updateBitmask) { + int userModifiedFields = zenRule.zenPolicyUserModifiedFields; if (oldPolicy.getPriorityMessageSenders() != newPolicy.getPriorityMessageSenders()) { userModifiedFields |= ZenPolicy.FIELD_MESSAGES; } @@ -1178,66 +1187,47 @@ public class ZenModeHelper { != newPolicy.getVisualEffectNotificationList()) { userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_NOTIFICATION_LIST; } + zenRule.zenPolicyUserModifiedFields = userModifiedFields; } - - // After all bitmask changes have been made, sets the bitmask. - return new ZenPolicy.Builder(newPolicy).setUserModifiedFields(userModifiedFields).build(); } /** - * Modifies {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule. - * Returns a {@link ZenDeviceEffects} based on {@code oldEffects}, but with fields updated to - * match {@code newEffects} where they differ, and updating the internal user-modified bitmask - * to track these changes, if applicable based on {@code origin}. - * <ul> - * <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are - * intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them - * out; if it's an update, we preserve the previous values. - * </ul> + * Modifies the {@link ZenDeviceEffects} associated to a new or updated ZenRule. + * + * <p>The new value is {@code newEffects}, while the user-modified bitmask is updated to reflect + * the changes being applied (if applicable, i.e. if the update is from the user). + * + * <p>Apps cannot turn on hidden effects (those tagged as {@code @hide}), so those fields are + * treated especially: for a new rule, they are blanked out; for an updated rule, previous + * values are preserved. */ - @Nullable - private static ZenDeviceEffects updateZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects, - @Nullable ZenDeviceEffects newEffects, - boolean isFromApp, - boolean updateBitmask) { + private static void updateZenDeviceEffects(ZenRule zenRule, + @Nullable ZenDeviceEffects newEffects, boolean isFromApp, boolean updateBitmask) { if (newEffects == null) { - return null; + zenRule.zenDeviceEffects = null; + return; } - // Since newEffects is not null, we want to adopt all the new provided device effects. - ZenDeviceEffects.Builder builder = new ZenDeviceEffects.Builder(newEffects); + ZenDeviceEffects oldEffects = zenRule.zenDeviceEffects != null + ? zenRule.zenDeviceEffects + : new ZenDeviceEffects.Builder().build(); if (isFromApp) { - if (oldEffects != null) { - // We can do this because we know we don't need to update the bitmask FROM_APP. - return builder - .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness()) - .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake()) - .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake()) - .setShouldDisableTouch(oldEffects.shouldDisableTouch()) - .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage()) - .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze()) - .build(); - } else { - return builder - .setShouldDisableAutoBrightness(false) - .setShouldDisableTapToWake(false) - .setShouldDisableTiltToWake(false) - .setShouldDisableTouch(false) - .setShouldMinimizeRadioUsage(false) - .setShouldMaximizeDoze(false) - .build(); - } + // Don't allow apps to toggle hidden effects. + newEffects = new ZenDeviceEffects.Builder(newEffects) + .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness()) + .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake()) + .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake()) + .setShouldDisableTouch(oldEffects.shouldDisableTouch()) + .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage()) + .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze()) + .build(); } - // If oldEffects is null, we compare against the default device effects object when - // determining which fields in the bitmask should be marked as updated. - if (oldEffects == null) { - oldEffects = new ZenDeviceEffects.Builder().build(); - } + zenRule.zenDeviceEffects = newEffects; - int userModifiedFields = oldEffects.getUserModifiedFields(); if (updateBitmask) { + int userModifiedFields = zenRule.zenDeviceEffectsUserModifiedFields; if (oldEffects.shouldDisplayGrayscale() != newEffects.shouldDisplayGrayscale()) { userModifiedFields |= ZenDeviceEffects.FIELD_GRAYSCALE; } @@ -1270,11 +1260,8 @@ public class ZenModeHelper { if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) { userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE; } + zenRule.zenDeviceEffectsUserModifiedFields = userModifiedFields; } - - // Since newEffects is not null, we want to adopt all the new provided device effects. - // Set the usermodifiedFields value separately, to reflect the updated bitmask. - return builder.setUserModifiedFields(userModifiedFields).build(); } private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) { @@ -1293,7 +1280,6 @@ public class ZenModeHelper { .setOwner(rule.component) .setConfigurationActivity(rule.configurationActivity) .setTriggerDescription(rule.triggerDescription) - .setUserModifiedFields(rule.userModifiedFields) .build(); } else { azr = new AutomaticZenRule(rule.name, rule.component, @@ -2369,6 +2355,19 @@ public class ZenModeHelper { return null; } } + + /** Checks that the {@code origin} supplied to a ZenModeHelper "API" method makes sense. */ + private static void requirePublicOrigin(String method, @ConfigChangeOrigin int origin) { + if (!Flags.modesApi()) { + return; + } + checkArgument(origin == UPDATE_ORIGIN_APP || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI + || origin == UPDATE_ORIGIN_USER, + "Expected one of UPDATE_ORIGIN_APP, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, or " + + "UPDATE_ORIGIN_USER for %s, but received '%s'.", + method, origin); + } + private final class Metrics extends Callback { private static final String COUNTER_MODE_PREFIX = "dnd_mode_"; private static final String COUNTER_TYPE_PREFIX = "dnd_type_"; diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index 9ce3cb3abe4a..f6e7ef3d50e9 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -41,13 +41,14 @@ import android.system.Os; import android.system.StructStat; import android.util.Slog; import android.util.SparseArray; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoParseException; import com.android.internal.annotations.GuardedBy; import com.android.server.BootReceiver; import com.android.server.ServiceThread; import com.android.server.os.TombstoneProtos.Cause; import com.android.server.os.TombstoneProtos.Tombstone; -import com.android.server.os.protobuf.CodedInputStream; import libcore.io.IoUtils; @@ -129,21 +130,18 @@ public final class NativeTombstoneManager { return; } + String processName = "UNKNOWN"; final boolean isProtoFile = filename.endsWith(".pb"); - if (!isProtoFile) { - return; - } + File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb"); - Optional<ParsedTombstone> parsedTombstone = handleProtoTombstone(path, true); + Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile); if (parsedTombstone.isPresent()) { - BootReceiver.addTombstoneToDropBox( - mContext, path, parsedTombstone.get().getTombstone(), - parsedTombstone.get().getProcessName(), mTmpFileLock); + processName = parsedTombstone.get().getProcessName(); } + BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock); } - private Optional<ParsedTombstone> handleProtoTombstone( - File path, boolean addToList) { + private Optional<TombstoneFile> handleProtoTombstone(File path, boolean addToList) { final String filename = path.getName(); if (!filename.endsWith(".pb")) { Slog.w(TAG, "unexpected tombstone name: " + path); @@ -173,7 +171,7 @@ public final class NativeTombstoneManager { return Optional.empty(); } - final Optional<ParsedTombstone> parsedTombstone = TombstoneFile.parse(pfd); + final Optional<TombstoneFile> parsedTombstone = TombstoneFile.parse(pfd); if (!parsedTombstone.isPresent()) { IoUtils.closeQuietly(pfd); return Optional.empty(); @@ -186,7 +184,7 @@ public final class NativeTombstoneManager { previous.dispose(); } - mTombstones.put(number, parsedTombstone.get().getTombstoneFile()); + mTombstones.put(number, parsedTombstone.get()); } } @@ -334,27 +332,6 @@ public final class NativeTombstoneManager { } } - static class ParsedTombstone { - TombstoneFile mTombstoneFile; - Tombstone mTombstone; - ParsedTombstone(TombstoneFile tombstoneFile, Tombstone tombstone) { - mTombstoneFile = tombstoneFile; - mTombstone = tombstone; - } - - public String getProcessName() { - return mTombstoneFile.getProcessName(); - } - - public TombstoneFile getTombstoneFile() { - return mTombstoneFile; - } - - public Tombstone getTombstone() { - return mTombstone; - } - } - static class TombstoneFile { final ParcelFileDescriptor mPfd; @@ -437,21 +414,67 @@ public final class NativeTombstoneManager { } } - static Optional<ParsedTombstone> parse(ParcelFileDescriptor pfd) { - Tombstone tombstoneProto; - try (FileInputStream is = new FileInputStream(pfd.getFileDescriptor())) { - final byte[] tombstoneBytes = is.readAllBytes(); + static Optional<TombstoneFile> parse(ParcelFileDescriptor pfd) { + final FileInputStream is = new FileInputStream(pfd.getFileDescriptor()); + final ProtoInputStream stream = new ProtoInputStream(is); + + int pid = 0; + int uid = 0; + String processName = null; + String crashReason = ""; + String selinuxLabel = ""; + + try { + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) Tombstone.PID: + pid = stream.readInt(Tombstone.PID); + break; + + case (int) Tombstone.UID: + uid = stream.readInt(Tombstone.UID); + break; + + case (int) Tombstone.COMMAND_LINE: + if (processName == null) { + processName = stream.readString(Tombstone.COMMAND_LINE); + } + break; - tombstoneProto = Tombstone.parseFrom( - CodedInputStream.newInstance(tombstoneBytes)); - } catch (IOException ex) { + case (int) Tombstone.CAUSES: + if (!crashReason.equals("")) { + // Causes appear in decreasing order of likelihood. For now we only + // want the most likely crash reason here, so ignore all others. + break; + } + long token = stream.start(Tombstone.CAUSES); + cause: + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) Cause.HUMAN_READABLE: + crashReason = stream.readString(Cause.HUMAN_READABLE); + break cause; + + default: + break; + } + } + stream.end(token); + break; + + case (int) Tombstone.SELINUX_LABEL: + selinuxLabel = stream.readString(Tombstone.SELINUX_LABEL); + break; + + default: + break; + } + } + } catch (IOException | ProtoParseException ex) { Slog.e(TAG, "Failed to parse tombstone", ex); return Optional.empty(); } - int pid = tombstoneProto.getPid(); - int uid = tombstoneProto.getUid(); - if (!UserHandle.isApp(uid)) { Slog.e(TAG, "Tombstone's UID (" + uid + ") not an app, ignoring"); return Optional.empty(); @@ -468,7 +491,6 @@ public final class NativeTombstoneManager { final int userId = UserHandle.getUserId(uid); final int appId = UserHandle.getAppId(uid); - String selinuxLabel = tombstoneProto.getSelinuxLabel(); if (!selinuxLabel.startsWith("u:r:untrusted_app")) { Slog.e(TAG, "Tombstone has invalid selinux label (" + selinuxLabel + "), ignoring"); return Optional.empty(); @@ -480,30 +502,11 @@ public final class NativeTombstoneManager { result.mAppId = appId; result.mPid = pid; result.mUid = uid; - result.mProcessName = getCmdLineProcessName(tombstoneProto); + result.mProcessName = processName == null ? "" : processName; result.mTimestampMs = timestampMs; - result.mCrashReason = getCrashReason(tombstoneProto); + result.mCrashReason = crashReason; - return Optional.of(new ParsedTombstone(result, tombstoneProto)); - } - - private static String getCmdLineProcessName(Tombstone tombstoneProto) { - for (String cmdline : tombstoneProto.getCommandLineList()) { - if (cmdline != null) { - return cmdline; - } - } - return ""; - } - - private static String getCrashReason(Tombstone tombstoneProto) { - for (Cause cause : tombstoneProto.getCausesList()) { - if (cause.getHumanReadable() != null - && !cause.getHumanReadable().equals("")) { - return cause.getHumanReadable(); - } - } - return ""; + return Optional.of(result); } public IParcelFileDescriptorRetriever getPfdRetriever() { diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index 7f0aadce3143..c110fb67b54f 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -75,11 +75,9 @@ public class BackgroundInstallControlService extends SystemService { private static final int MSG_PACKAGE_ADDED = 1; private static final int MSG_PACKAGE_REMOVED = 2; - private final Context mContext; private final BinderService mBinderService; private final PackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; - private final UsageStatsManagerInternal mUsageStatsManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final Handler mHandler; private final File mDiskFile; @@ -99,14 +97,14 @@ public class BackgroundInstallControlService extends SystemService { @VisibleForTesting BackgroundInstallControlService(@NonNull Injector injector) { super(injector.getContext()); - mContext = injector.getContext(); mPackageManager = injector.getPackageManager(); mPackageManagerInternal = injector.getPackageManagerInternal(); mPermissionManager = injector.getPermissionManager(); mHandler = new EventHandler(injector.getLooper(), this); mDiskFile = injector.getDiskFile(); - mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal(); - mUsageStatsManagerInternal.registerListener( + UsageStatsManagerInternal usageStatsManagerInternal = + injector.getUsageStatsManagerInternal(); + usageStatsManagerInternal.registerListener( (userId, event) -> mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, userId, diff --git a/services/core/java/com/android/server/pm/DataLoaderManagerService.java b/services/core/java/com/android/server/pm/DataLoaderManagerService.java index 888e1c26206c..c25cea65376b 100644 --- a/services/core/java/com/android/server/pm/DataLoaderManagerService.java +++ b/services/core/java/com/android/server/pm/DataLoaderManagerService.java @@ -47,7 +47,6 @@ import java.util.List; public class DataLoaderManagerService extends SystemService { private static final String TAG = "DataLoaderManager"; private final Context mContext; - private final HandlerThread mThread; private final Handler mHandler; private final DataLoaderManagerBinderService mBinderService; private final SparseArray<DataLoaderServiceConnection> mServiceConnections = @@ -57,10 +56,10 @@ public class DataLoaderManagerService extends SystemService { super(context); mContext = context; - mThread = new HandlerThread(TAG); - mThread.start(); + HandlerThread thread = new HandlerThread(TAG); + thread.start(); - mHandler = new Handler(mThread.getLooper()); + mHandler = new Handler(thread.getLooper()); mBinderService = new DataLoaderManagerBinderService(); } diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index e3bbd2d8d17e..f987d4ae8999 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -107,9 +107,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @Nullable private final ComponentName mInstantAppResolverSettingsComponent; - @NonNull - private final String mRequiredSupplementalProcessPackage; - @Nullable private final String mServicesExtensionPackageName; @@ -125,7 +122,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @NonNull PackageInstallerService installerService, @NonNull PackageProperty packageProperty, @NonNull ComponentName resolveComponentName, @Nullable ComponentName instantAppResolverSettingsComponent, - @NonNull String requiredSupplementalProcessPackage, @Nullable String servicesExtensionPackageName, @Nullable String sharedSystemSharedLibraryPackageName) { mService = service; @@ -140,7 +136,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { mPackageProperty = packageProperty; mResolveComponentName = resolveComponentName; mInstantAppResolverSettingsComponent = instantAppResolverSettingsComponent; - mRequiredSupplementalProcessPackage = requiredSupplementalProcessPackage; mServicesExtensionPackageName = servicesExtensionPackageName; mSharedSystemSharedLibraryPackageName = sharedSystemSharedLibraryPackageName; } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 991555495ad2..ac826afc1d22 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -32,6 +32,8 @@ import static android.content.pm.LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS; import static android.content.pm.LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS; import static android.content.pm.LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS; +import static com.android.server.pm.PackageArchiver.isArchivingEnabled; + import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -56,7 +58,6 @@ import android.content.IntentSender; import android.content.LocusId; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.Flags; import android.content.pm.ILauncherApps; import android.content.pm.IOnAppsChangedListener; import android.content.pm.IPackageInstallerCallback; @@ -507,7 +508,8 @@ public class LauncherAppsService extends SystemService { if (!canAccessProfile(userId, "cannot get shouldHideFromSuggestions")) { return false; } - if (Flags.archiving() && packageName != null && isPackageArchived(packageName, user)) { + if (isArchivingEnabled() && packageName != null + && isPackageArchived(packageName, user)) { return true; } if (mPackageManagerInternal.filterAppAccess( @@ -530,7 +532,7 @@ public class LauncherAppsService extends SystemService { .addCategory(Intent.CATEGORY_LAUNCHER) .setPackage(packageName), user); - if (Flags.archiving()) { + if (isArchivingEnabled()) { launcherActivities = getActivitiesForArchivedApp(packageName, user, launcherActivities); } @@ -701,7 +703,7 @@ public class LauncherAppsService extends SystemService { callingUid, user.getIdentifier()); if (activityInfo == null) { - if (Flags.archiving()) { + if (isArchivingEnabled()) { return getMatchingArchivedAppActivityInfo(component, user); } return null; @@ -984,7 +986,7 @@ public class LauncherAppsService extends SystemService { long callingFlag = PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; - if (Flags.archiving()) { + if (isArchivingEnabled()) { callingFlag |= PackageManager.MATCH_ARCHIVED_PACKAGES; } final PackageInfo info = @@ -1457,7 +1459,7 @@ public class LauncherAppsService extends SystemService { if (!canAccessProfile(user.getIdentifier(), "Cannot check component")) { return false; } - if (Flags.archiving() && component != null && component.getPackageName() != null) { + if (isArchivingEnabled() && component != null && component.getPackageName() != null) { List<LauncherActivityInfoInternal> archiveActivities = generateLauncherActivitiesForArchivedApp(component.getPackageName(), user); if (!archiveActivities.isEmpty()) { @@ -1788,7 +1790,7 @@ public class LauncherAppsService extends SystemService { } if (!canLaunch && includeArchivedApps - && Flags.archiving() + && isArchivingEnabled() && getMatchingArchivedAppActivityInfo(component, user) != null) { launchIntent.setPackage(null); launchIntent.setComponent(component); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 3e5759a88213..09a91eda483a 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -51,12 +51,13 @@ import android.content.Intent; import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.ArchivedActivityParcel; +import android.content.pm.ArchivedPackageInfo; import android.content.pm.ArchivedPackageParcel; +import android.content.pm.Flags; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.DeleteFlags; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.VersionedPackage; @@ -77,6 +78,7 @@ import android.os.ParcelableException; import android.os.Process; import android.os.RemoteException; import android.os.SELinux; +import android.os.SystemProperties; import android.os.UserHandle; import android.text.TextUtils; import android.util.ExceptionUtils; @@ -172,12 +174,15 @@ public class PackageArchiver { return userState.getArchiveState() != null && !userState.isInstalled(); } + public static boolean isArchivingEnabled() { + return Flags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false); + } + void requestArchive( @NonNull String packageName, @NonNull String callerPackageName, @NonNull IntentSender intentSender, - @NonNull UserHandle userHandle, - @DeleteFlags int flags) { + @NonNull UserHandle userHandle) { Objects.requireNonNull(packageName); Objects.requireNonNull(callerPackageName); Objects.requireNonNull(intentSender); @@ -217,7 +222,7 @@ public class PackageArchiver { new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), callerPackageName, - DELETE_ARCHIVE | DELETE_KEEP_DATA | flags, + DELETE_ARCHIVE | DELETE_KEEP_DATA, intentSender, userId, binderUid); @@ -402,23 +407,30 @@ public class PackageArchiver { installerPackage, /* flags= */ 0, userId); if (installerInfo == null) { // Should never happen because we just fetched the installerInfo. - Slog.e(TAG, "Couldnt find installer " + installerPackage); + Slog.e(TAG, "Couldn't find installer " + installerPackage); return null; } + final int iconSize = mContext.getSystemService( + ActivityManager.class).getLauncherLargeIconSize(); + + var info = new ArchivedPackageInfo(archivedPackage); try { - var packageName = archivedPackage.packageName; - var mainActivities = archivedPackage.archivedActivities; - List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.length); - for (int i = 0, size = mainActivities.length; i < size; ++i) { - var mainActivity = mainActivities[i]; - Path iconPath = storeIconForParcel(packageName, mainActivity, userId, i); + var packageName = info.getPackageName(); + var mainActivities = info.getLauncherActivities(); + List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); + for (int i = 0, size = mainActivities.size(); i < size; ++i) { + var mainActivity = mainActivities.get(i); + Path iconPath = storeDrawable( + packageName, mainActivity.getIcon(), userId, i, iconSize); + Path monochromePath = storeDrawable( + packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize); ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( - mainActivity.title, - mainActivity.originalComponentName, + mainActivity.getLabel().toString(), + mainActivity.getComponentName(), iconPath, - null); + monochromePath); archiveActivityInfos.add(activityInfo); } @@ -452,21 +464,6 @@ public class PackageArchiver { return new ArchiveState(archiveActivityInfos, installerTitle); } - // TODO(b/298452477) Handle monochrome icons. - private static Path storeIconForParcel(String packageName, ArchivedActivityParcel mainActivity, - @UserIdInt int userId, int index) throws IOException { - if (mainActivity.iconBitmap == null) { - return null; - } - File iconsDir = createIconsDir(packageName, userId); - File iconFile = new File(iconsDir, index + ".png"); - try (FileOutputStream out = new FileOutputStream(iconFile)) { - out.write(mainActivity.iconBitmap); - out.flush(); - } - return iconFile.toPath(); - } - @VisibleForTesting Path storeIcon(String packageName, LauncherActivityInfo mainActivity, @UserIdInt int userId, int index, int iconSize) throws IOException { @@ -475,9 +472,18 @@ public class PackageArchiver { // The app doesn't define an icon. No need to store anything. return null; } + return storeDrawable(packageName, mainActivity.getIcon(/* density= */ 0), userId, index, + iconSize); + } + + private static Path storeDrawable(String packageName, @Nullable Drawable iconDrawable, + @UserIdInt int userId, int index, int iconSize) throws IOException { + if (iconDrawable == null) { + return null; + } File iconsDir = createIconsDir(packageName, userId); File iconFile = new File(iconsDir, index + ".png"); - Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0), iconSize); + Bitmap icon = drawableToBitmap(iconDrawable, iconSize); try (FileOutputStream out = new FileOutputStream(iconFile)) { // Note: Quality is ignored for PNGs. if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index fdcd28b0ed02..7bf9fe7aa7e2 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -30,6 +30,7 @@ import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; import static android.os.Process.INVALID_UID; import static android.os.Process.SYSTEM_UID; +import static com.android.server.pm.PackageArchiver.isArchivingEnabled; import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; @@ -826,7 +827,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } params.installFlags &= ~PackageManager.INSTALL_UNARCHIVE; - if (Flags.archiving() && params.appPackageName != null) { + if (isArchivingEnabled() && params.appPackageName != null) { PackageStateInternal ps = mPm.snapshotComputer().getPackageStateInternal( params.appPackageName, SYSTEM_UID); if (ps != null @@ -1034,7 +1035,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private int getExistingDraftSessionIdInternal(int installerUid, SessionParams sessionParams, int userId) { String appPackageName = sessionParams.appPackageName; - if (!Flags.archiving() || installerUid == INVALID_UID || appPackageName == null) { + if (!isArchivingEnabled() || installerUid == INVALID_UID || appPackageName == null) { return SessionInfo.INVALID_ID; } @@ -1407,14 +1408,11 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext, statusReceiver, versionedPackage.getPackageName(), canSilentlyInstallPackage, userId, mPackageArchiver, flags); - final boolean shouldShowConfirmationDialog = - (flags & PackageManager.DELETE_SHOW_DIALOG) != 0; - if (!shouldShowConfirmationDialog - && mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES) - == PackageManager.PERMISSION_GRANTED) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES) + == PackageManager.PERMISSION_GRANTED) { // Sweet, call straight through! mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); - } else if (!shouldShowConfirmationDialog && canSilentlyInstallPackage) { + } else if (canSilentlyInstallPackage) { // Allow the device owner and affiliated profile owner to silently delete packages // Need to clear the calling identity to get DELETE_PACKAGES permission final long ident = Binder.clearCallingIdentity(); @@ -1656,10 +1654,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements @NonNull String packageName, @NonNull String callerPackageName, @NonNull IntentSender intentSender, - @NonNull UserHandle userHandle, - @DeleteFlags int flags) { - mPackageArchiver.requestArchive(packageName, callerPackageName, intentSender, - userHandle, flags); + @NonNull UserHandle userHandle) { + mPackageArchiver.requestArchive(packageName, callerPackageName, intentSender, userHandle); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 5225529ef001..c5b5a761497d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -4659,8 +4659,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPreferredActivityHelper, mResolveIntentHelper, mDomainVerificationManager, mDomainVerificationConnection, mInstallerService, mPackageProperty, mResolveComponentName, mInstantAppResolverSettingsComponent, - mRequiredSdkSandboxPackage, mServicesExtensionPackageName, - mSharedSystemSharedLibraryPackageName); + mServicesExtensionPackageName, mSharedSystemSharedLibraryPackageName); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 5724ee0d94e9..ca00c84da724 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -4645,7 +4645,7 @@ class PackageManagerShellCommand extends ShellCommand { try { mInterface.getPackageInstaller().requestArchive(packageName, /* callerPackageName= */ "", receiver.getIntentSender(), - new UserHandle(translatedUserId), 0); + new UserHandle(translatedUserId)); } catch (Exception e) { pw.println("Failure [" + e.getMessage() + "]"); return 1; diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java index 98533725371f..524252c1469f 100644 --- a/services/core/java/com/android/server/pm/ProtectedPackages.java +++ b/services/core/java/com/android/server/pm/ProtectedPackages.java @@ -57,11 +57,8 @@ public class ProtectedPackages { @GuardedBy("this") private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>(); - private final Context mContext; - public ProtectedPackages(Context context) { - mContext = context; - mDeviceProvisioningPackage = mContext.getResources().getString( + mDeviceProvisioningPackage = context.getResources().getString( R.string.config_deviceProvisioningPackage); } diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 7bd6a43969ba..70352be01096 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -67,7 +67,6 @@ final class RemovePackageHelper { private final PackageManagerService mPm; private final IncrementalManager mIncrementalManager; private final Installer mInstaller; - private final UserManagerInternal mUserManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final SharedLibrariesImpl mSharedLibraries; private final AppDataHelper mAppDataHelper; @@ -79,7 +78,6 @@ final class RemovePackageHelper { mPm = pm; mIncrementalManager = mPm.mInjector.getIncrementalManager(); mInstaller = mPm.mInjector.getInstaller(); - mUserManagerInternal = mPm.mInjector.getUserManagerInternal(); mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal(); mSharedLibraries = mPm.mInjector.getSharedLibrariesImpl(); mAppDataHelper = appDataHelper; @@ -394,8 +392,7 @@ final class RemovePackageHelper { // Delete from mSettings final SparseBooleanArray changedUsers = new SparseBooleanArray(); synchronized (mPm.mLock) { - mPm.mSettings.removePackageLPw(packageName); - outInfo.mIsAppIdRemoved = true; + outInfo.mIsAppIdRemoved = mPm.mSettings.removePackageAndAppIdLPw(packageName); if (!mPm.mSettings.isDisabledSystemPackageLPr(packageName)) { final SharedUserSetting sus = mPm.mSettings.getSharedUserSettingLPr(deletedPs); // If we don't have a disabled system package to reinstall, the package is @@ -487,8 +484,6 @@ final class RemovePackageHelper { synchronized (mPm.mInstallLock) { cleanUpResourcesLI(codeFile, instructionSets); } - // TODO: open logging to help debug, will delete or add debug flag - Slog.d(TAG, "cleanUpResources for " + codeFile); if (packageName == null) { return; } diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index b286b12dcf7d..a97652c8e167 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -92,7 +92,6 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; -import com.android.internal.pm.parsing.pkg.PackageImpl; import com.android.internal.pm.pkg.component.ParsedComponent; import com.android.internal.pm.pkg.component.ParsedIntentInfo; import com.android.internal.pm.pkg.component.ParsedPermission; @@ -1460,22 +1459,28 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile return false; } - int removePackageLPw(String name) { + + /** + * Remove package from mPackages and its corresponding AppId. + * + * @return True if the AppId has been removed. + * False if the app doesn't exist, or if the app has a shared UID and there are other apps that + * still use the same shared UID even after the target app is removed. + */ + boolean removePackageAndAppIdLPw(String name) { final PackageSetting p = mPackages.remove(name); if (p != null) { removeInstallerPackageStatus(name); SharedUserSetting sharedUserSetting = getSharedUserSettingLPr(p); if (sharedUserSetting != null) { sharedUserSetting.removePackage(p); - if (checkAndPruneSharedUserLPw(sharedUserSetting, false)) { - return sharedUserSetting.mAppId; - } + return checkAndPruneSharedUserLPw(sharedUserSetting, false); } else { removeAppIdLPw(p.getAppId()); - return p.getAppId(); + return true; } } - return -1; + return false; } /** diff --git a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java index 7f6f684e0b68..aa52522cfe46 100644 --- a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java +++ b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java @@ -31,7 +31,6 @@ import java.io.PrintWriter; * The access to it must be guarded with the shortcut manager lock. */ public class ShortcutNonPersistentUser { - private final ShortcutService mService; private final int mUserId; @@ -49,8 +48,7 @@ public class ShortcutNonPersistentUser { */ private final ArraySet<String> mHostPackageSet = new ArraySet<>(); - public ShortcutNonPersistentUser(ShortcutService service, int userId) { - mService = service; + public ShortcutNonPersistentUser(int userId) { mUserId = userId; } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index e993d9e5b724..d644235b8714 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -33,6 +33,7 @@ import android.app.appsearch.SearchResult; import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.app.appsearch.SetSchemaRequest; +import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -47,6 +48,7 @@ import android.graphics.drawable.Icon; import android.os.Binder; import android.os.PersistableBundle; import android.os.StrictMode; +import android.os.SystemClock; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.ArraySet; @@ -192,6 +194,9 @@ class ShortcutPackage extends ShortcutPackageItem { private long mLastKnownForegroundElapsedTime; @GuardedBy("mLock") + private long mLastReportedTime; + + @GuardedBy("mLock") private boolean mIsAppSearchSchemaUpToDate; private ShortcutPackage(ShortcutUser shortcutUser, @@ -1673,6 +1678,26 @@ class ShortcutPackage extends ShortcutPackageItem { return condition[0]; } + void reportShortcutUsed(@NonNull final UsageStatsManagerInternal usageStatsManagerInternal, + @NonNull final String shortcutId) { + synchronized (mLock) { + final long currentTS = SystemClock.elapsedRealtime(); + final ShortcutService s = mShortcutUser.mService; + if (currentTS - mLastReportedTime > s.mSaveDelayMillis) { + mLastReportedTime = currentTS; + } else { + return; + } + final long token = s.injectClearCallingIdentity(); + try { + usageStatsManagerInternal.reportShortcutUsage(getPackageName(), shortcutId, + getPackageUserId()); + } finally { + s.injectRestoreCallingIdentity(token); + } + } + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) { pw.println(); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 446c6293aa35..c23d2abf0853 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -371,7 +371,7 @@ public class ShortcutService extends IShortcutService.Stub { private CompressFormat mIconPersistFormat; private int mIconPersistQuality; - private int mSaveDelayMillis; + int mSaveDelayMillis; private final IPackageManager mIPackageManager; private final PackageManagerInternal mPackageManagerInternal; @@ -1378,7 +1378,7 @@ public class ShortcutService extends IShortcutService.Stub { ShortcutNonPersistentUser getNonPersistentUserLocked(@UserIdInt int userId) { ShortcutNonPersistentUser ret = mShortcutNonPersistentUsers.get(userId); if (ret == null) { - ret = new ShortcutNonPersistentUser(this, userId); + ret = new ShortcutNonPersistentUser(userId); mShortcutNonPersistentUsers.put(userId, ret); } return ret; @@ -2291,7 +2291,7 @@ public class ShortcutService extends IShortcutService.Stub { packageShortcutsChanged(ps, changedShortcuts, removedShortcuts); - reportShortcutUsedInternal(packageName, shortcut.getId(), userId); + ps.reportShortcutUsed(mUsageStatsManagerInternal, shortcut.getId()); verifyStates(); } @@ -2695,25 +2695,17 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, String.format("reportShortcutUsed: Shortcut %s package %s used on user %d", shortcutId, packageName, userId)); } + final ShortcutPackage ps; synchronized (mLock) { throwIfUserLockedL(userId); - final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); + ps = getPackageShortcutsForPublisherLocked(packageName, userId); if (ps.findShortcutById(shortcutId) == null) { Log.w(TAG, String.format("reportShortcutUsed: package %s doesn't have shortcut %s", packageName, shortcutId)); return; } } - reportShortcutUsedInternal(packageName, shortcutId, userId); - } - - private void reportShortcutUsedInternal(String packageName, String shortcutId, int userId) { - final long token = injectClearCallingIdentity(); - try { - mUsageStatsManagerInternal.reportShortcutUsage(packageName, shortcutId, userId); - } finally { - injectRestoreCallingIdentity(token); - } + ps.reportShortcutUsed(mUsageStatsManagerInternal, shortcutId); } @Override @@ -5202,13 +5194,11 @@ public class ShortcutService extends IShortcutService.Stub { } // Injection point. - @VisibleForTesting long injectClearCallingIdentity() { return Binder.clearCallingIdentity(); } // Injection point. - @VisibleForTesting void injectRestoreCallingIdentity(long token) { Binder.restoreCallingIdentity(token); } diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java index 4c42c2dd0850..1d414011cff3 100644 --- a/services/core/java/com/android/server/pm/UserDataPreparer.java +++ b/services/core/java/com/android/server/pm/UserDataPreparer.java @@ -141,7 +141,7 @@ class UserDataPreparer { // If internal storage of the system user fails to prepare on first boot, then // things are *really* broken, so we might as well reboot to recovery right away. try { - Log.wtf(TAG, "prepareUserData failed for user " + userId, e); + Log.e(TAG, "prepareUserData failed for user " + userId, e); if (isNewUser && userId == UserHandle.USER_SYSTEM && volumeUuid == null) { RecoverySystem.rebootPromptAndWipeUserData(mContext, "failed to prepare internal storage for system user"); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c1b74898e5ae..c94111c31ef4 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -16,6 +16,8 @@ package com.android.server.pm; +import static android.content.Intent.ACTION_SCREEN_OFF; +import static android.content.Intent.ACTION_SCREEN_ON; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY; @@ -41,11 +43,13 @@ import android.annotation.ColorRes; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.StringRes; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityManagerNative; +import android.app.ActivityOptions; import android.app.BroadcastOptions; import android.app.IActivityManager; import android.app.IStopUserCallback; @@ -73,8 +77,10 @@ import android.content.pm.UserProperties; import android.content.pm.parsing.FrameworkParsingPackageUtils; import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Bitmap; import android.multiuser.Flags; +import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -82,6 +88,7 @@ import android.os.Debug; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.IProgressListener; import android.os.IUserManager; @@ -90,6 +97,7 @@ import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.PersistableBundle; +import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; @@ -173,6 +181,8 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -295,6 +305,12 @@ public class UserManagerService extends IUserManager.Stub { private static final long BOOT_USER_SET_TIMEOUT_MS = 300_000; + /** + * The time duration (in milliseconds) post device inactivity after which the private space + * should be auto-locked if the corresponding settings option is selected by the user. + */ + private static final long PRIVATE_SPACE_AUTO_LOCK_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; + // Tron counters private static final String TRON_GUEST_CREATED = "users_guest_created"; private static final String TRON_USER_CREATED = "users_user_created"; @@ -320,6 +336,8 @@ public class UserManagerService extends IUserManager.Stub { private final Handler mHandler; + private final ThreadPoolExecutor mInternalExecutor; + private final File mUsersDir; private final File mUserListFile; @@ -521,6 +539,36 @@ public class UserManagerService extends IUserManager.Stub { private final LockPatternUtils mLockPatternUtils; + private KeyguardManager.KeyguardLockedStateListener mKeyguardLockedStateListener; + + /** Token to identify and remove already scheduled private space auto-lock messages */ + private static final Object PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN = new Object(); + + /** Content observer to get callbacks for privte space autolock settings changes */ + private final SettingsObserver mPrivateSpaceAutoLockSettingsObserver; + + private final class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (isAutoLockForPrivateSpaceEnabled()) { + final String path = uri.getLastPathSegment(); + if (TextUtils.equals(path, Settings.Secure.PRIVATE_SPACE_AUTO_LOCK)) { + int autoLockPreference = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, + getMainUserIdUnchecked()); + Slog.i(LOG_TAG, "Auto-lock settings changed to " + autoLockPreference); + setOrUpdateAutoLockPreferenceForPrivateProfile(autoLockPreference); + } + } + } + } + private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK = "com.android.server.pm.DISABLE_QUIET_MODE_AFTER_UNLOCK"; @@ -533,12 +581,168 @@ public class UserManagerService extends IUserManager.Stub { final IntentSender target = intent.getParcelableExtra(Intent.EXTRA_INTENT, android.content.IntentSender.class); final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL); final String callingPackage = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); - // Call setQuietModeEnabled on bg thread to avoid ANR - BackgroundThread.getHandler().post(() -> - setQuietModeEnabled(userId, false, target, callingPackage)); + setQuietModeEnabledAsync(userId, false, target, callingPackage); } }; + /** Checks if the device inactivity broadcast receiver is already registered*/ + private boolean mIsDeviceInactivityBroadcastReceiverRegistered = false; + + private final BroadcastReceiver mDeviceInactivityBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isAutoLockForPrivateSpaceEnabled()) { + if (ACTION_SCREEN_OFF.equals(intent.getAction())) { + maybeScheduleMessageToAutoLockPrivateSpace(); + } else if (ACTION_SCREEN_ON.equals(intent.getAction())) { + // Remove any queued messages since the device is interactive again + mHandler.removeCallbacksAndMessages(PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN); + } + } + } + }; + + @VisibleForTesting + void maybeScheduleMessageToAutoLockPrivateSpace() { + // No action needed if auto-lock on inactivity not selected + int privateSpaceAutoLockPreference = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, + getMainUserIdUnchecked()); + if (privateSpaceAutoLockPreference + != Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY) { + return; + } + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId != UserHandle.USER_NULL) { + scheduleMessageToAutoLockPrivateSpace(privateProfileUserId, + PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN, + PRIVATE_SPACE_AUTO_LOCK_INACTIVITY_TIMEOUT_MS); + } + } + + @VisibleForTesting + void scheduleMessageToAutoLockPrivateSpace(int userId, Object token, + long delayInMillis) { + mHandler.postDelayed(() -> { + final PowerManager powerManager = mContext.getSystemService(PowerManager.class); + if (powerManager != null && !powerManager.isInteractive()) { + Slog.i(LOG_TAG, "Auto-locking private space with user-id " + userId); + setQuietModeEnabledAsync(userId, true, + /* target */ null, mContext.getPackageName()); + } else { + Slog.i(LOG_TAG, "Device is interactive, skipping auto-lock"); + } + }, token, delayInMillis); + } + + @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) + private void initializeAndRegisterKeyguardLockedStateListener() { + mKeyguardLockedStateListener = this::tryAutoLockingPrivateSpaceOnKeyguardChanged; + // Register with keyguard to send locked state events to the listener initialized above + try { + final KeyguardManager keyguardManager = + mContext.getSystemService(KeyguardManager.class); + Slog.i(LOG_TAG, "Adding keyguard locked state listener"); + keyguardManager.addKeyguardLockedStateListener(new HandlerExecutor(mHandler), + mKeyguardLockedStateListener); + } catch (Exception e) { + Slog.e(LOG_TAG, "Error adding keyguard locked listener ", e); + } + } + + @VisibleForTesting + @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) + void setOrUpdateAutoLockPreferenceForPrivateProfile( + @Settings.Secure.PrivateSpaceAutoLockOption int autoLockPreference) { + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId == UserHandle.USER_NULL) { + Slog.e(LOG_TAG, "Auto-lock preference updated but private space user not found"); + return; + } + + if (autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY) { + // Register inactivity broadcast + if (!mIsDeviceInactivityBroadcastReceiverRegistered) { + Slog.i(LOG_TAG, "Registering device inactivity broadcast receivers"); + mContext.registerReceiver(mDeviceInactivityBroadcastReceiver, + new IntentFilter(ACTION_SCREEN_OFF), + null, mHandler); + + mContext.registerReceiver(mDeviceInactivityBroadcastReceiver, + new IntentFilter(ACTION_SCREEN_ON), + null, mHandler); + + mIsDeviceInactivityBroadcastReceiverRegistered = true; + } + } else { + // Unregister device inactivity broadcasts + if (mIsDeviceInactivityBroadcastReceiverRegistered) { + Slog.i(LOG_TAG, "Removing device inactivity broadcast receivers"); + mHandler.removeCallbacksAndMessages(PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN); + mContext.unregisterReceiver(mDeviceInactivityBroadcastReceiver); + mIsDeviceInactivityBroadcastReceiverRegistered = false; + } + } + + if (autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK) { + // Initialize and add keyguard state listener + initializeAndRegisterKeyguardLockedStateListener(); + } else { + // Remove keyguard state listener + try { + final KeyguardManager keyguardManager = + mContext.getSystemService(KeyguardManager.class); + Slog.i(LOG_TAG, "Removing keyguard locked state listener"); + keyguardManager.removeKeyguardLockedStateListener(mKeyguardLockedStateListener); + } catch (Exception e) { + Slog.e(LOG_TAG, "Error adding keyguard locked state listener ", e); + } + } + } + + @VisibleForTesting + void tryAutoLockingPrivateSpaceOnKeyguardChanged(boolean isKeyguardLocked) { + if (isAutoLockForPrivateSpaceEnabled()) { + int autoLockPreference = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, + getMainUserIdUnchecked()); + boolean isAutoLockOnDeviceLockSelected = + autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK; + if (isKeyguardLocked && isAutoLockOnDeviceLockSelected) { + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId != UserHandle.USER_NULL) { + Slog.i(LOG_TAG, "Auto-locking private space with user-id " + + privateProfileUserId); + setQuietModeEnabledAsync(privateProfileUserId, + /* enableQuietMode */true, /* target */ null, + mContext.getPackageName()); + } + } + } + } + + @VisibleForTesting + void setQuietModeEnabledAsync(@UserIdInt int userId, boolean enableQuietMode, + IntentSender target, @Nullable String callingPackage) { + if (android.multiuser.Flags.moveQuietModeOperationsToSeparateThread()) { + // Call setQuietModeEnabled on a separate thread. Calling this operation on the main + // thread can cause ANRs, posting on a BackgroundThread can result in delays + Slog.d(LOG_TAG, "Calling setQuietModeEnabled for user " + userId + + " on a separate thread"); + mInternalExecutor.execute(() -> setQuietModeEnabled(userId, enableQuietMode, target, + callingPackage)); + } else { + // Call setQuietModeEnabled on bg thread to avoid ANR + BackgroundThread.getHandler().post( + () -> setQuietModeEnabled(userId, enableQuietMode, target, + callingPackage) + ); + } + } + /** * Cache the owner name string, since it could be read repeatedly on a critical code path * but hit by slow IO. This could be eliminated once we have the cached UserInfo in place. @@ -587,7 +791,10 @@ public class UserManagerService extends IUserManager.Stub { public void onFinished(int id, Bundle extras) { mHandler.post(() -> { try { - mContext.startIntentSender(mTarget, null, 0, 0, 0); + ActivityOptions activityOptions = + ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + mContext.startIntentSender(mTarget, null, 0, 0, 0, activityOptions.toBundle()); } catch (IntentSender.SendIntentException e) { Slog.e(LOG_TAG, "Failed to start the target in the callback", e); } @@ -762,6 +969,8 @@ public class UserManagerService extends IUserManager.Stub { mPackagesLock = packagesLock; mUsers = users != null ? users : new SparseArray<>(); mHandler = new MainHandler(); + mInternalExecutor = new ThreadPoolExecutor(/* corePoolSize */ 0, /* maximumPoolSize */ 1, + /* keepAliveTime */ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); mUserVisibilityMediator = new UserVisibilityMediator(mHandler); mUserDataPreparer = userDataPreparer; mUserTypes = UserTypeFactory.getUserTypes(); @@ -786,9 +995,15 @@ public class UserManagerService extends IUserManager.Stub { mLockPatternUtils = new LockPatternUtils(mContext); mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING); mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null; + mPrivateSpaceAutoLockSettingsObserver = new SettingsObserver(mHandler); emulateSystemUserModeIfNeeded(); } + private static boolean isAutoLockForPrivateSpaceEnabled() { + return android.os.Flags.allowPrivateProfile() + && Flags.supportAutolockForPrivateSpace(); + } + void systemReady() { mAppOpsService = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); @@ -805,6 +1020,22 @@ public class UserManagerService extends IUserManager.Stub { new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED), null, mHandler); + if (isAutoLockForPrivateSpaceEnabled()) { + + int mainUserId = getMainUserIdUnchecked(); + if (mainUserId != UserHandle.USER_NULL) { + mContext.getContentResolver().registerContentObserverAsUser( + Settings.Secure.getUriFor( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), false, + mPrivateSpaceAutoLockSettingsObserver, UserHandle.of(mainUserId)); + + setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, mainUserId)); + } + } + markEphemeralUsersForRemoval(); } @@ -969,6 +1200,18 @@ public class UserManagerService extends IUserManager.Stub { return UserHandle.USER_NULL; } + private @UserIdInt int getPrivateProfileUserId() { + synchronized (mUsersLock) { + for (int userId : getUserIds()) { + UserInfo userInfo = getUserInfoLU(userId); + if (userInfo != null && userInfo.isPrivateProfile()) { + return userInfo.id; + } + } + } + return UserHandle.USER_NULL; + } + @Override public void setBootUser(@UserIdInt int userId) { checkCreateUsersPermission("Set boot user"); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index ee40e18b7d9f..40f226435194 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -77,7 +77,6 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.TriFunction; import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider; import com.android.server.pm.pkg.AndroidPackage; @@ -114,9 +113,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { /** Internal connection to the package manager */ private final PackageManagerInternal mPackageManagerInt; - /** Internal connection to the user manager */ - private final UserManagerInternal mUserManagerInt; - /** Map of OneTimePermissionUserManagers keyed by userId */ @GuardedBy("mLock") @NonNull @@ -148,7 +144,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { mContext = context; mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class); - mUserManagerInt = LocalServices.getService(UserManagerInternal.class); mAppOpsManager = context.getSystemService(AppOpsManager.class); mAttributionSourceRegistry = new AttributionSourceRegistry(context); @@ -1098,12 +1093,10 @@ public class PermissionManagerService extends IPermissionManager.Stub { private static final AtomicInteger sAttributionChainIds = new AtomicInteger(0); private final @NonNull Context mContext; - private final @NonNull AppOpsManager mAppOpsManager; private final @NonNull PermissionManagerServiceInternal mPermissionManagerServiceInternal; PermissionCheckerService(@NonNull Context context) { mContext = context; - mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mPermissionManagerServiceInternal = LocalServices.getService(PermissionManagerServiceInternal.class); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 3afba39ad4af..6a5736269e51 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -279,7 +279,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt @NonNull private final int[] mGlobalGids; - private final HandlerThread mHandlerThread; private final Handler mHandler; private final Context mContext; private final MetricsLogger mMetricsLogger = new MetricsLogger(); @@ -432,10 +431,10 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } } - mHandlerThread = new ServiceThread(TAG, + HandlerThread handlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper()); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); Watchdog.getInstance().addThread(mHandler); SystemConfig systemConfig = SystemConfig.getInstance(); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index e2269535d931..a172de0bb0ff 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -4413,8 +4413,8 @@ public final class PowerManagerService extends SystemService private boolean setPowerModeInternal(int mode, boolean enabled) { // Maybe filter the event. - if (mBatterySaverStateMachine == null || (mode == Mode.LAUNCH && enabled - && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled())) { + if (mode == Mode.LAUNCH && enabled && mBatterySaverStateMachine != null + && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled()) { return false; } return mNativeWrapper.nativeSetPowerMode(mode, enabled); diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java index 375ef6150830..a4dbce6b2631 100644 --- a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java +++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java @@ -65,6 +65,7 @@ import com.android.internal.widget.LockSettingsInternal; import com.android.internal.widget.RebootEscrowListener; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.Watchdog; import com.android.server.pm.ApexManager; import com.android.server.recoverysystem.hal.BootControlHIDL; @@ -112,6 +113,9 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo private static final int SOCKET_CONNECTION_MAX_RETRY = 30; + /** How long to pause the watchdog for when rebooting the device. */ + private static final int REBOOT_WATCHDOG_PAUSE_DURATION_MS = 20_000; + static final String REQUEST_LSKF_TIMESTAMP_PREF_SUFFIX = "_request_lskf_timestamp"; static final String REQUEST_LSKF_COUNT_PREF_SUFFIX = "_request_lskf_count"; @@ -899,7 +903,8 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo // Clear the metrics prefs after a successful RoR reboot. mInjector.getMetricsPrefs().deletePrefsFile(); - + Watchdog.getInstance().pauseWatchingCurrentThreadFor( + REBOOT_WATCHDOG_PAUSE_DURATION_MS, "reboot can be slow"); PowerManager pm = mInjector.getPowerManager(); pm.reboot(reason); return RESUME_ON_REBOOT_REBOOT_ERROR_UNSPECIFIED; diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index e3eb5b52bc2e..e5a8a6dd2a3a 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -44,6 +44,9 @@ import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; +import android.hardware.biometrics.SensorProperties; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -155,6 +158,8 @@ public class TrustManagerService extends SystemService { private final LockPatternUtils mLockPatternUtils; private final UserManager mUserManager; private final ActivityManager mActivityManager; + private FingerprintManager mFingerprintManager; + private FaceManager mFaceManager; private VirtualDeviceManagerInternal mVirtualDeviceManager; private enum TrustState { @@ -292,6 +297,8 @@ public class TrustManagerService extends SystemService { mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true); mReceiver.register(mContext); mLockPatternUtils.registerStrongAuthTracker(mStrongAuthTracker); + mFingerprintManager = mContext.getSystemService(FingerprintManager.class); + mFaceManager = mContext.getSystemService(FaceManager.class); } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { mTrustAgentsCanRun = true; refreshAgentList(UserHandle.USER_ALL); @@ -893,7 +900,19 @@ public class TrustManagerService extends SystemService { private void notifyKeystoreOfDeviceLockState(int userId, boolean isLocked) { if (isLocked) { - Authorization.onDeviceLocked(userId, getBiometricSids(userId)); + if (android.security.Flags.fixUnlockedDeviceRequiredKeysV2()) { + // A profile with unified challenge is unlockable not by its own biometrics and + // trust agents, but rather by those of the parent user. Therefore, when protecting + // the profile's UnlockedDeviceRequired keys, we must use the parent's list of + // biometric SIDs and weak unlock methods, not the profile's. + int authUserId = mLockPatternUtils.isProfileWithUnifiedChallenge(userId) + ? resolveProfileParent(userId) : userId; + + Authorization.onDeviceLocked(userId, getBiometricSids(authUserId), + isWeakUnlockMethodEnabled(authUserId)); + } else { + Authorization.onDeviceLocked(userId, getBiometricSids(userId), false); + } } else { // Notify Keystore that the device is now unlocked for the user. Note that for unlocks // with LSKF, this is redundant with the call from LockSettingsService which provides @@ -1440,16 +1459,59 @@ public class TrustManagerService extends SystemService { if (biometricManager == null) { return new long[0]; } - if (android.security.Flags.fixUnlockedDeviceRequiredKeysV2() - && mLockPatternUtils.isProfileWithUnifiedChallenge(userId)) { - // Profiles with unified challenge have their own set of biometrics, but the device - // unlock happens via the parent user. In this case Keystore needs to be given the list - // of biometric SIDs from the parent user, not the profile. - userId = resolveProfileParent(userId); - } return biometricManager.getAuthenticatorIds(userId); } + // Returns whether the device can become unlocked for the specified user via one of that user's + // non-strong biometrics or trust agents. This assumes that the device is currently locked, or + // is becoming locked, for the user. + private boolean isWeakUnlockMethodEnabled(int userId) { + + // Check whether the system currently allows the use of non-strong biometrics for the user, + // *and* the user actually has a non-strong biometric enrolled. + // + // The biometrics framework ostensibly supports multiple sensors per modality. However, + // that feature is unused and untested. So, we simply consider one sensor per modality. + // + // Also, currently we just consider fingerprint and face, matching Keyguard. If Keyguard + // starts supporting other biometric modalities, this will need to be updated. + if (mStrongAuthTracker.isBiometricAllowedForUser(/* isStrongBiometric= */ false, userId)) { + DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); + int disabledFeatures = dpm.getKeyguardDisabledFeatures(null, userId); + + if (mFingerprintManager != null + && (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) == 0 + && mFingerprintManager.hasEnrolledTemplates(userId) + && isWeakOrConvenienceSensor( + mFingerprintManager.getSensorProperties().get(0))) { + Slog.i(TAG, "User is unlockable by non-strong fingerprint auth"); + return true; + } + + if (mFaceManager != null + && (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FACE) == 0 + && mFaceManager.hasEnrolledTemplates(userId) + && isWeakOrConvenienceSensor(mFaceManager.getSensorProperties().get(0))) { + Slog.i(TAG, "User is unlockable by non-strong face auth"); + return true; + } + } + + // Check whether it's possible for the device to be actively unlocked by a trust agent. + if (getUserTrustStateInner(userId) == TrustState.TRUSTABLE + || (isAutomotive() && isTrustUsuallyManagedInternal(userId))) { + Slog.i(TAG, "User is unlockable by trust agent"); + return true; + } + + return false; + } + + private static boolean isWeakOrConvenienceSensor(SensorProperties sensor) { + return sensor.getSensorStrength() == SensorProperties.STRENGTH_WEAK + || sensor.getSensorStrength() == SensorProperties.STRENGTH_CONVENIENCE; + } + // User lifecycle @Override diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index d903ad4d9f0d..73fc8e9cfbc4 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -2280,6 +2280,26 @@ public final class TvInputManagerService extends SystemService { } @Override + public void setVideoFrozen(IBinder sessionToken, boolean isFrozen, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "setVideoFrozen"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId) + .setVideoFrozen(isFrozen); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in setVideoFrozen", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void setTvMessageEnabled(IBinder sessionToken, int type, boolean enabled, int userId) { final int callingUid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 0467d0cd351d..7ab075e2f3a7 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -38,7 +38,16 @@ import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; +import android.media.tv.ad.ITvAdClient; import android.media.tv.ad.ITvAdManager; +import android.media.tv.ad.ITvAdManagerCallback; +import android.media.tv.ad.ITvAdService; +import android.media.tv.ad.ITvAdServiceCallback; +import android.media.tv.ad.ITvAdSession; +import android.media.tv.ad.ITvAdSessionCallback; +import android.media.tv.ad.TvAdService; +import android.media.tv.ad.TvAdServiceInfo; +import android.media.tv.flags.Flags; import android.media.tv.interactive.AppLinkInfo; import android.media.tv.interactive.ITvInteractiveAppClient; import android.media.tv.interactive.ITvInteractiveAppManager; @@ -110,6 +119,8 @@ public class TvInteractiveAppManagerService extends SystemService { @GuardedBy("mLock") private boolean mGetServiceListCalled = false; @GuardedBy("mLock") + private boolean mGetAdServiceListCalled = false; + @GuardedBy("mLock") private boolean mGetAppLinkInfoListCalled = false; private final UserManager mUserManager; @@ -256,6 +267,141 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void buildTvAdServiceListLocked(int userId, String[] updatedPackages) { + if (!Flags.enableAdServiceFw()) { + return; + } + UserState userState = getOrCreateUserStateLocked(userId); + userState.mPackageSet.clear(); + + if (DEBUG) { + Slogf.d(TAG, "buildTvAdServiceListLocked"); + } + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = pm.queryIntentServicesAsUser( + new Intent(TvAdService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + userId); + List<TvAdServiceInfo> serviceList = new ArrayList<>(); + + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_AD_SERVICE.equals(si.permission)) { + Slog.w(TAG, "Skipping TV AD service " + si.name + + ": it does not require the permission " + + android.Manifest.permission.BIND_TV_AD_SERVICE); + continue; + } + + ComponentName component = new ComponentName(si.packageName, si.name); + try { + TvAdServiceInfo info = new TvAdServiceInfo(mContext, component); + serviceList.add(info); + } catch (Exception e) { + Slogf.e(TAG, "failed to load TV AD service " + si.name, e); + continue; + } + userState.mPackageSet.add(si.packageName); + } + + // sort the service list by service id + Collections.sort(serviceList, Comparator.comparing(TvAdServiceInfo::getId)); + Map<String, TvAdServiceState> adServiceMap = new HashMap<>(); + for (TvAdServiceInfo info : serviceList) { + String serviceId = info.getId(); + if (DEBUG) { + Slogf.d(TAG, "add " + serviceId); + } + TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId); + if (adServiceState == null) { + adServiceState = new TvAdServiceState(); + } + adServiceState.mInfo = info; + adServiceState.mUid = getAdServiceUid(info); + adServiceState.mComponentName = info.getComponent(); + adServiceMap.put(serviceId, adServiceState); + } + + for (String serviceId : adServiceMap.keySet()) { + if (!userState.mAdServiceMap.containsKey(serviceId)) { + notifyAdServiceAddedLocked(userState, serviceId); + } else if (updatedPackages != null) { + // Notify the package updates + ComponentName component = adServiceMap.get(serviceId).mInfo.getComponent(); + for (String updatedPackage : updatedPackages) { + if (component.getPackageName().equals(updatedPackage)) { + updateAdServiceConnectionLocked(component, userId); + notifyAdServiceUpdatedLocked(userState, serviceId); + break; + } + } + } + } + + for (String serviceId : userState.mAdServiceMap.keySet()) { + if (!adServiceMap.containsKey(serviceId)) { + TvAdServiceInfo info = userState.mAdServiceMap.get(serviceId).mInfo; + AdServiceState serviceState = userState.mAdServiceStateMap.get(info.getComponent()); + if (serviceState != null) { + abortPendingCreateAdSessionRequestsLocked(serviceState, serviceId, userId); + } + notifyAdServiceRemovedLocked(userState, serviceId); + } + } + + userState.mIAppMap.clear(); + userState.mAdServiceMap = adServiceMap; + } + + @GuardedBy("mLock") + private void notifyAdServiceAddedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceAddedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceAdded(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report added AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") + private void notifyAdServiceRemovedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceRemovedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceRemoved(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report removed AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") + private void notifyAdServiceUpdatedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceUpdatedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceUpdated(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report updated AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") private void notifyInteractiveAppServiceAddedLocked(UserState userState, String iAppServiceId) { if (DEBUG) { Slog.d(TAG, "notifyInteractiveAppServiceAddedLocked(iAppServiceId=" @@ -340,6 +486,16 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private int getAdServiceUid(TvAdServiceInfo info) { + try { + return getContext().getPackageManager().getApplicationInfo( + info.getServiceInfo().packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + Slogf.w(TAG, "Unable to get UID for " + info, e); + return Process.INVALID_UID; + } + } + @Override public void onStart() { if (DEBUG) { @@ -357,6 +513,7 @@ public class TvInteractiveAppManagerService extends SystemService { synchronized (mLock) { buildTvInteractiveAppServiceListLocked(mCurrentUserId, null); buildAppLinkInfoLocked(mCurrentUserId); + buildTvAdServiceListLocked(mCurrentUserId, null); } } } @@ -372,6 +529,14 @@ public class TvInteractiveAppManagerService extends SystemService { } } } + private void buildTvAdServiceList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvAdServiceListLocked(userId, packages); + } + } + } @Override public void onPackageUpdateFinished(String packageName, int uid) { @@ -379,6 +544,7 @@ public class TvInteractiveAppManagerService extends SystemService { // This callback is invoked when the TV interactive App service is reinstalled. // In this case, isReplacing() always returns true. buildTvInteractiveAppServiceList(new String[] { packageName }); + buildTvAdServiceList(new String[] { packageName }); } @Override @@ -390,6 +556,7 @@ public class TvInteractiveAppManagerService extends SystemService { // available. if (isReplacing()) { buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); } } @@ -403,6 +570,7 @@ public class TvInteractiveAppManagerService extends SystemService { } if (isReplacing()) { buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); } } @@ -418,6 +586,7 @@ public class TvInteractiveAppManagerService extends SystemService { return; } buildTvInteractiveAppServiceList(null); + buildTvAdServiceList(null); } @Override @@ -476,6 +645,7 @@ public class TvInteractiveAppManagerService extends SystemService { mCurrentUserId = userId; buildTvInteractiveAppServiceListLocked(userId, null); buildAppLinkInfoLocked(userId); + buildTvAdServiceListLocked(userId, null); } } @@ -562,6 +732,7 @@ public class TvInteractiveAppManagerService extends SystemService { mRunningProfiles.add(userId); buildTvInteractiveAppServiceListLocked(userId, null); buildAppLinkInfoLocked(userId); + buildTvAdServiceListLocked(userId, null); } @GuardedBy("mLock") @@ -619,7 +790,19 @@ public class TvInteractiveAppManagerService extends SystemService { Slog.e(TAG, "error in onSessionReleased", e); } } - removeSessionStateLocked(state.mSessionToken, state.mUserId); + removeAdSessionStateLocked(state.mSessionToken, state.mUserId); + } + + @GuardedBy("mLock") + private void clearAdSessionAndNotifyClientLocked(AdSessionState state) { + if (state.mClient != null) { + try { + state.mClient.onSessionReleased(state.mSeq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onSessionReleased", e); + } + } + removeAdSessionStateLocked(state.mSessionToken, state.mUserId); } private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId, @@ -655,6 +838,44 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private AdSessionState getAdSessionStateLocked( + IBinder sessionToken, int callingUid, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + return getAdSessionStateLocked(sessionToken, callingUid, userState); + } + + @GuardedBy("mLock") + private AdSessionState getAdSessionStateLocked(IBinder sessionToken, int callingUid, + UserState userState) { + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (sessionState == null) { + throw new SessionNotFoundException("Session state not found for token " + sessionToken); + } + // Only the application that requested this session or the system can access it. + if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.mCallingUid) { + throw new SecurityException("Illegal access to the session with token " + sessionToken + + " from uid " + callingUid); + } + return sessionState; + } + + @GuardedBy("mLock") + private ITvAdSession getAdSessionLocked( + IBinder sessionToken, int callingUid, int userId) { + return getAdSessionLocked(getAdSessionStateLocked(sessionToken, callingUid, userId)); + } + + @GuardedBy("mLock") + private ITvAdSession getAdSessionLocked(AdSessionState sessionState) { + ITvAdSession session = sessionState.mSession; + if (session == null) { + throw new IllegalStateException("Session not yet created for token " + + sessionState.mSessionToken); + } + return session; + } + + @GuardedBy("mLock") private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) { UserState userState = getOrCreateUserStateLocked(userId); return getSessionStateLocked(sessionToken, callingUid, userState); @@ -691,10 +912,200 @@ public class TvInteractiveAppManagerService extends SystemService { return session; } private final class TvAdBinderService extends ITvAdManager.Stub { + + @Override + public List<TvAdServiceInfo> getTvAdServiceList(int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "getTvAdServiceList"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!mGetAdServiceListCalled) { + buildTvAdServiceListLocked(userId, null); + mGetAdServiceListCalled = true; + } + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + List<TvAdServiceInfo> adServiceList = new ArrayList<>(); + for (TvAdServiceState state : userState.mAdServiceMap.values()) { + adServiceList.add(state.mInfo); + } + return adServiceList; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void createSession(final ITvAdClient client, final String serviceId, String type, + int seq, int userId) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, + userId, "createSession"); + final long identity = Binder.clearCallingIdentity(); + + try { + synchronized (mLock) { + if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) { + // Only current user and its running profiles can create sessions. + // Let the client get onConnectionFailed callback for this case. + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + TvAdServiceState adState = userState.mAdMap.get(serviceId); + if (adState == null) { + Slogf.w(TAG, "Failed to find state for serviceId=" + serviceId); + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + AdServiceState serviceState = + userState.mAdServiceStateMap.get(adState.mComponentName); + if (serviceState == null) { + int tasUid = PackageManager.getApplicationInfoAsUserCached( + adState.mComponentName.getPackageName(), 0, resolvedUserId).uid; + serviceState = new AdServiceState( + adState.mComponentName, serviceId, resolvedUserId); + userState.mAdServiceStateMap.put(adState.mComponentName, serviceState); + } + // Send a null token immediately while reconnecting. + if (serviceState.mReconnecting) { + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + + // Create a new session token and a session state. + IBinder sessionToken = new Binder(); + AdSessionState sessionState = new AdSessionState(sessionToken, serviceId, type, + adState.mComponentName, client, seq, callingUid, + callingPid, resolvedUserId); + + // Add them to the global session state map of the current user. + userState.mAdSessionStateMap.put(sessionToken, sessionState); + + // Also, add them to the session state map of the current service. + serviceState.mSessionTokens.add(sessionToken); + + if (serviceState.mService != null) { + if (!createAdSessionInternalLocked(serviceState.mService, sessionToken, + resolvedUserId)) { + removeAdSessionStateLocked(sessionToken, resolvedUserId); + } + } else { + updateAdServiceConnectionLocked(adState.mComponentName, resolvedUserId); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void releaseSession(IBinder sessionToken, int userId) { + if (DEBUG) { + Slogf.d(TAG, "releaseSession(sessionToken=" + sessionToken + ")"); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "releaseSession"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + releaseSessionLocked(sessionToken, callingUid, resolvedUserId); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void setSurface(IBinder sessionToken, Surface surface, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "setSurface"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).setSurface(surface); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in setSurface", e); + } + } + } finally { + if (surface != null) { + // surface is not used in TvInteractiveAppManagerService. + surface.release(); + } + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width, + int height, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "dispatchSurfaceChanged"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + AdSessionState sessionState = getAdSessionStateLocked( + sessionToken, callingUid, resolvedUserId); + getAdSessionLocked(sessionState).dispatchSurfaceChanged(format, width, + height); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in dispatchSurfaceChanged", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public void startAdService(IBinder sessionToken, int userId) { } + @Override + public void registerCallback(final ITvAdManagerCallback callback, int userId) { + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, + "registerCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final UserState userState = getOrCreateUserStateLocked(resolvedUserId); + if (!userState.mAdCallbacks.register(callback)) { + Slog.e(TAG, "client process has already died"); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void unregisterCallback(ITvAdManagerCallback callback, int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "unregisterCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + userState.mAdCallbacks.unregister(callback); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } private final class BinderService extends ITvInteractiveAppManager.Stub { @@ -927,7 +1338,7 @@ public class TvInteractiveAppManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - releaseSessionLocked(sessionToken, callingUid, resolvedUserId); + releaseAdSessionLocked(sessionToken, callingUid, resolvedUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -1471,6 +1882,32 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void sendSelectedTrackInfo(IBinder sessionToken, List<TvTrackInfo> tracks, + int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendSelectedTrackInfo(tracks=%s)", tracks.toString()); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendSelectedTrackInfo"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendSelectedTrackInfo(tracks); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendSelectedTrackInfo", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) { if (DEBUG) { Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId); @@ -2134,6 +2571,17 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void sendAdSessionTokenToClientLocked( + ITvAdClient client, String serviceId, IBinder sessionToken, + InputChannel channel, int seq) { + try { + client.onSessionCreated(serviceId, sessionToken, channel, seq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onSessionCreated", e); + } + } + + @GuardedBy("mLock") private boolean createSessionInternalLocked( ITvInteractiveAppService service, IBinder sessionToken, int userId) { UserState userState = getOrCreateUserStateLocked(userId); @@ -2163,6 +2611,58 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private boolean createAdSessionInternalLocked( + ITvAdService service, IBinder sessionToken, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (DEBUG) { + Slogf.d(TAG, "createAdSessionInternalLocked(iAppServiceId=" + + sessionState.mAdServiceId + ")"); + } + InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString()); + + // Set up a callback to send the session token. + ITvAdSessionCallback callback = new AdSessionCallback(sessionState, channels); + + boolean created = true; + // Create a session. When failed, send a null token immediately. + try { + service.createSession( + channels[1], callback, sessionState.mAdServiceId, sessionState.mType); + } catch (RemoteException e) { + Slogf.e(TAG, "error in createSession", e); + sendAdSessionTokenToClientLocked(sessionState.mClient, sessionState.mAdServiceId, null, + null, sessionState.mSeq); + created = false; + } + channels[1].dispose(); + return created; + } + + @GuardedBy("mLock") + @Nullable + private AdSessionState releaseAdSessionLocked( + IBinder sessionToken, int callingUid, int userId) { + AdSessionState sessionState = null; + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, userId); + UserState userState = getOrCreateUserStateLocked(userId); + if (sessionState.mSession != null) { + sessionState.mSession.asBinder().unlinkToDeath(sessionState, 0); + sessionState.mSession.release(); + } + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in releaseSession", e); + } finally { + if (sessionState != null) { + sessionState.mSession = null; + } + } + removeAdSessionStateLocked(sessionToken, userId); + return sessionState; + } + + @GuardedBy("mLock") @Nullable private SessionState releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) { SessionState sessionState = null; @@ -2215,6 +2715,36 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void removeAdSessionStateLocked(IBinder sessionToken, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + + // Remove the session state from the global session state map of the current user. + AdSessionState sessionState = userState.mAdSessionStateMap.remove(sessionToken); + + if (sessionState == null) { + Slogf.e(TAG, "sessionState null, no more remove session action!"); + return; + } + + // Also remove the session token from the session token list of the current client and + // service. + ClientState clientState = userState.mClientStateMap.get(sessionState.mClient.asBinder()); + if (clientState != null) { + clientState.mSessionTokens.remove(sessionToken); + if (clientState.isEmpty()) { + userState.mClientStateMap.remove(sessionState.mClient.asBinder()); + sessionState.mClient.asBinder().unlinkToDeath(clientState, 0); + } + } + + AdServiceState serviceState = userState.mAdServiceStateMap.get(sessionState.mComponent); + if (serviceState != null) { + serviceState.mSessionTokens.remove(sessionToken); + } + updateAdServiceConnectionLocked(sessionState.mComponent, userId); + } + + @GuardedBy("mLock") private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState, String iAppServiceId, int userId) { // Let clients know the create session requests are failed. @@ -2237,6 +2767,28 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void abortPendingCreateAdSessionRequestsLocked(AdServiceState serviceState, + String serviceId, int userId) { + // Let clients know the create session requests are failed. + UserState userState = getOrCreateUserStateLocked(userId); + List<AdSessionState> sessionsToAbort = new ArrayList<>(); + for (IBinder sessionToken : serviceState.mSessionTokens) { + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (sessionState.mSession == null + && (serviceState == null + || sessionState.mAdServiceId.equals(serviceId))) { + sessionsToAbort.add(sessionState); + } + } + for (AdSessionState sessionState : sessionsToAbort) { + removeAdSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId); + sendAdSessionTokenToClientLocked(sessionState.mClient, + sessionState.mAdServiceId, null, null, sessionState.mSeq); + } + updateAdServiceConnectionLocked(serviceState.mComponent, userId); + } + + @GuardedBy("mLock") private void updateServiceConnectionLocked(ComponentName component, int userId) { UserState userState = getOrCreateUserStateLocked(userId); ServiceState serviceState = userState.mServiceStateMap.get(component); @@ -2284,10 +2836,64 @@ public class TvInteractiveAppManagerService extends SystemService { } } + @GuardedBy("mLock") + private void updateAdServiceConnectionLocked(ComponentName component, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + AdServiceState serviceState = userState.mAdServiceStateMap.get(component); + if (serviceState == null) { + return; + } + if (serviceState.mReconnecting) { + if (!serviceState.mSessionTokens.isEmpty()) { + // wait until all the sessions are removed. + return; + } + serviceState.mReconnecting = false; + } + + boolean shouldBind = (!serviceState.mSessionTokens.isEmpty()) + || (!serviceState.mPendingAppLinkCommand.isEmpty()); + + if (serviceState.mService == null && shouldBind) { + // This means that the service is not yet connected but its state indicates that we + // have pending requests. Then, connect the service. + if (serviceState.mBound) { + // We have already bound to the service so we don't try to bind again until after we + // unbind later on. + return; + } + if (DEBUG) { + Slogf.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")"); + } + + Intent i = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component); + serviceState.mBound = mContext.bindServiceAsUser( + i, serviceState.mConnection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, + new UserHandle(userId)); + } else if (serviceState.mService != null && !shouldBind) { + // This means that the service is already connected but its state indicates that we have + // nothing to do with it. Then, disconnect the service. + if (DEBUG) { + Slogf.d(TAG, "unbindService(service=" + component + ")"); + } + mContext.unbindService(serviceState.mConnection); + userState.mAdServiceStateMap.remove(component); + } + } + private static final class UserState { private final int mUserId; + // A mapping from the TV AD service ID to its TvAdServiceState. + private Map<String, TvAdServiceState> mAdMap = new HashMap<>(); + // A mapping from the name of a TV Interactive App service to its state. + private final Map<ComponentName, AdServiceState> mAdServiceStateMap = new HashMap<>(); + // A mapping from the token of a TV Interactive App session to its state. + private final Map<IBinder, AdSessionState> mAdSessionStateMap = new HashMap<>(); // A mapping from the TV Interactive App ID to its TvInteractiveAppState. private Map<String, TvInteractiveAppState> mIAppMap = new HashMap<>(); + // A mapping from the TV AD service ID to its TvAdServiceState. + private Map<String, TvAdServiceState> mAdServiceMap = new HashMap<>(); // A mapping from the token of a client to its state. private final Map<IBinder, ClientState> mClientStateMap = new HashMap<>(); // A mapping from the name of a TV Interactive App service to its state. @@ -2299,6 +2905,8 @@ public class TvInteractiveAppManagerService extends SystemService { private final Set<String> mPackageSet = new HashSet<>(); // A list of all app link infos. private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>(); + private final RemoteCallbackList<ITvAdManagerCallback> mAdCallbacks = + new RemoteCallbackList<>(); // A list of callbacks. private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks = @@ -2317,7 +2925,16 @@ public class TvInteractiveAppManagerService extends SystemService { private int mIAppNumber; } + private static final class TvAdServiceState { + private String mAdServiceId; + private ComponentName mComponentName; + private TvAdServiceInfo mInfo; + private int mUid; + private int mAdNumber; + } + private final class SessionState implements IBinder.DeathRecipient { + // TODO: rename SessionState and reorganize classes / methods of this file private final IBinder mSessionToken; private ITvInteractiveAppSession mSession; private final String mIAppServiceId; @@ -2359,6 +2976,49 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdSessionState implements IBinder.DeathRecipient { + private final IBinder mSessionToken; + private ITvAdSession mSession; + private final String mAdServiceId; + + private final String mType; + private final ITvAdClient mClient; + private final int mSeq; + private final ComponentName mComponent; + + // The UID of the application that created the session. + // The application is usually the TV app. + private final int mCallingUid; + + // The PID of the application that created the session. + // The application is usually the TV app. + private final int mCallingPid; + + private final int mUserId; + + private AdSessionState(IBinder sessionToken, String serviceId, String type, + ComponentName componentName, ITvAdClient client, int seq, + int callingUid, int callingPid, int userId) { + mSessionToken = sessionToken; + mAdServiceId = serviceId; + mType = type; + mComponent = componentName; + mClient = client; + mSeq = seq; + mCallingUid = callingUid; + mCallingPid = callingPid; + mUserId = userId; + } + + @Override + public void binderDied() { + synchronized (mLock) { + mSession = null; + clearAdSessionAndNotifyClientLocked(this); + } + } + } + private final class ClientState implements IBinder.DeathRecipient { private final List<IBinder> mSessionTokens = new ArrayList<>(); @@ -2429,6 +3089,29 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdServiceState { + private final List<IBinder> mSessionTokens = new ArrayList<>(); + private final ServiceConnection mConnection; + private final ComponentName mComponent; + private final String mAdServiceId; + private final List<Bundle> mPendingAppLinkCommand = new ArrayList<>(); + + private ITvAdService mService; + private AdServiceCallback mCallback; + private boolean mBound; + private boolean mReconnecting; + + private AdServiceState(ComponentName component, String tasId, int userId) { + mComponent = component; + mConnection = new AdServiceConnection(component, userId); + mAdServiceId = tasId; + } + + private void addPendingAppLinkCommand(Bundle command) { + mPendingAppLinkCommand.add(command); + } + } + private final class InteractiveAppServiceConnection implements ServiceConnection { private final ComponentName mComponent; private final int mUserId; @@ -2542,6 +3225,98 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdServiceConnection implements ServiceConnection { + private final ComponentName mComponent; + private final int mUserId; + + private AdServiceConnection(ComponentName component, int userId) { + mComponent = component; + mUserId = userId; + } + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (DEBUG) { + Slogf.d(TAG, "onServiceConnected(component=" + component + ")"); + } + synchronized (mLock) { + UserState userState = getUserStateLocked(mUserId); + if (userState == null) { + // The user was removed while connecting. + mContext.unbindService(this); + return; + } + AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent); + serviceState.mService = ITvAdService.Stub.asInterface(service); + + // Register a callback, if we need to. + if (serviceState.mCallback == null) { + serviceState.mCallback = new AdServiceCallback(mComponent, mUserId); + try { + serviceState.mService.registerCallback(serviceState.mCallback); + } catch (RemoteException e) { + Slog.e(TAG, "error in registerCallback", e); + } + } + + if (!serviceState.mPendingAppLinkCommand.isEmpty()) { + for (Iterator<Bundle> it = serviceState.mPendingAppLinkCommand.iterator(); + it.hasNext(); ) { + Bundle command = it.next(); + final long identity = Binder.clearCallingIdentity(); + try { + serviceState.mService.sendAppLinkCommand(command); + it.remove(); + } catch (RemoteException e) { + Slogf.e(TAG, "error in sendAppLinkCommand(" + command + + ") when onServiceConnected", e); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + List<IBinder> tokensToBeRemoved = new ArrayList<>(); + + // And create sessions, if any. + for (IBinder sessionToken : serviceState.mSessionTokens) { + if (!createAdSessionInternalLocked( + serviceState.mService, sessionToken, mUserId)) { + tokensToBeRemoved.add(sessionToken); + } + } + + for (IBinder sessionToken : tokensToBeRemoved) { + removeAdSessionStateLocked(sessionToken, mUserId); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (DEBUG) { + Slogf.d(TAG, "onServiceDisconnected(component=" + component + ")"); + } + if (!mComponent.equals(component)) { + throw new IllegalArgumentException("Mismatched ComponentName: " + + mComponent + " (expected), " + component + " (actual)."); + } + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(mUserId); + AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent); + if (serviceState != null) { + serviceState.mReconnecting = true; + serviceState.mBound = false; + serviceState.mService = null; + serviceState.mCallback = null; + + abortPendingCreateAdSessionRequestsLocked(serviceState, null, mUserId); + } + } + } + } + + private final class ServiceCallback extends ITvInteractiveAppServiceCallback.Stub { private final ComponentName mComponent; private final int mUserId; @@ -2567,6 +3342,17 @@ public class TvInteractiveAppManagerService extends SystemService { } } + + private final class AdServiceCallback extends ITvAdServiceCallback.Stub { + private final ComponentName mComponent; + private final int mUserId; + + AdServiceCallback(ComponentName component, int userId) { + mComponent = component; + mUserId = userId; + } + } + private final class SessionCallback extends ITvInteractiveAppSessionCallback.Stub { private final SessionState mSessionState; private final InputChannel[] mInputChannels; @@ -2798,6 +3584,23 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestSelectedTrackInfo() { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestSelectedTrackInfo"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestSelectedTrackInfo(mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestSelectedTrackInfo", e); + } + } + } + + @Override public void onRequestCurrentTvInputId() { synchronized (mLock) { if (DEBUG) { @@ -3110,6 +3913,85 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdSessionCallback extends ITvAdSessionCallback.Stub { + private final AdSessionState mSessionState; + private final InputChannel[] mInputChannels; + + AdSessionCallback(AdSessionState sessionState, InputChannel[] channels) { + mSessionState = sessionState; + mInputChannels = channels; + } + + @Override + public void onSessionCreated(ITvAdSession session) { + if (DEBUG) { + Slogf.d(TAG, "onSessionCreated(adServiceId=" + + mSessionState.mAdServiceId + ")"); + } + synchronized (mLock) { + mSessionState.mSession = session; + if (session != null && addAdSessionTokenToClientStateLocked(session)) { + sendAdSessionTokenToClientLocked( + mSessionState.mClient, + mSessionState.mAdServiceId, + mSessionState.mSessionToken, + mInputChannels[0], + mSessionState.mSeq); + } else { + removeAdSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId); + sendAdSessionTokenToClientLocked(mSessionState.mClient, + mSessionState.mAdServiceId, null, null, mSessionState.mSeq); + } + mInputChannels[0].dispose(); + } + } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + + ", right=" + right + ", bottom=" + bottom + ",)"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onLayoutSurface(left, top, right, bottom, + mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onLayoutSurface", e); + } + } + } + + @GuardedBy("mLock") + private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) { + try { + session.asBinder().linkToDeath(mSessionState, 0); + } catch (RemoteException e) { + Slogf.e(TAG, "session process has already died", e); + return false; + } + + IBinder clientToken = mSessionState.mClient.asBinder(); + UserState userState = getOrCreateUserStateLocked(mSessionState.mUserId); + ClientState clientState = userState.mClientStateMap.get(clientToken); + if (clientState == null) { + clientState = new ClientState(clientToken, mSessionState.mUserId); + try { + clientToken.linkToDeath(clientState, 0); + } catch (RemoteException e) { + Slogf.e(TAG, "client process has already died", e); + return false; + } + userState.mClientStateMap.put(clientToken, clientState); + } + clientState.mSessionTokens.add(mSessionState.mSessionToken); + return true; + } + } + private static class SessionNotFoundException extends IllegalArgumentException { SessionNotFoundException(String name) { super(name); diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index e3aba0f6bc6f..c15ac37565ff 100644 --- a/services/core/java/com/android/server/utils/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -111,7 +111,7 @@ public class AnrTimer<V> implements AutoCloseable { * but it can be changed for local testing. */ private static boolean anrTimerServiceEnabled() { - return Flags.anrTimerServiceEnabled(); + return Flags.anrTimerService(); } /** diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig index 489e21ab06ca..163116b9c5c7 100644 --- a/services/core/java/com/android/server/utils/flags.aconfig +++ b/services/core/java/com/android/server/utils/flags.aconfig @@ -1,7 +1,7 @@ package: "com.android.server.utils" flag { - name: "anr_timer_service_enabled" + name: "anr_timer_service" namespace: "system_performance" is_fixed_read_only: true description: "Feature flag for the ANR timer service" diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 3782b429f93a..26584315f15c 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1256,7 +1256,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // migrateStaticSystemToLockWallpaperLocked() and added to the lock wp map // if successful. WallpaperData lockWp = mLockWallpaperMap.get(mNewWallpaper.userId); - if (lockWp != null) { + if (lockWp != null && mOriginalSystem.connection != null) { // Successful rename, set old system+lock to the pending lock wp if (DEBUG) { Slog.v(TAG, "static system+lock to system success"); diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index 60dc4ff224bc..d95b431752ec 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -348,6 +348,10 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private void pinWebviewIfRequired(ApplicationInfo appInfo) { PinnerService pinnerService = LocalServices.getService(PinnerService.class); + if (pinnerService == null) { + // This happens in unit tests which do not have services. + return; + } int webviewPinQuota = pinnerService.getWebviewPinQuota(); if (webviewPinQuota <= 0) { return; diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 676203bc746a..2e0546eee8e7 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -778,17 +778,22 @@ class ActivityClientController extends IActivityClientController.Stub { try { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); + if (r == null) { + return false; + } // Create a transition if the activity is playing in case the below activity didn't // commit invisible. That's because if any activity below this one has changed its // visibility while playing transition, there won't able to commit visibility until // the running transition finish. - final Transition transition = r != null - && r.mTransitionController.inPlayingTransition(r) + final Transition transition = r.mTransitionController.isShellTransitionsEnabled() && !r.mTransitionController.isCollecting() ? r.mTransitionController.createTransition(TRANSIT_TO_BACK) : null; - final boolean changed = r != null && r.setOccludesParent(true); + final boolean changed = r.setOccludesParent(true); if (transition != null) { if (changed) { + // Always set as scene transition because it expects to be a jump-cut. + transition.setOverrideAnimation(TransitionInfo.AnimationOptions + .makeSceneTransitionAnimOptions(), null, null); r.mTransitionController.requestStartTransition(transition, null /*startTask */, null /* remoteTransition */, null /* displayChange */); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 69fbe6ba3c29..9b1f9c8441ad 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3093,7 +3093,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final boolean changed = occludesParent != mOccludesParent; mOccludesParent = occludesParent; setMainWindowOpaque(occludesParent); - mWmService.mWindowPlacerLocked.requestTraversal(); if (changed && task != null && !occludesParent) { getRootTask().convertActivityToTranslucent(this); diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index e7621ffe8e3c..182e1c133790 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.app.ActivityManager.INTENT_SENDER_ACTIVITY; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; @@ -144,22 +145,20 @@ class ActivityStartInterceptor { } private IntentSender createIntentSenderForOriginalIntent(int callingUid, int flags) { - Bundle bOptions = deferCrossProfileAppsAnimationIfNecessary(); + ActivityOptions activityOptions = deferCrossProfileAppsAnimationIfNecessary(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); final TaskFragment taskFragment = getLaunchTaskFragment(); // If the original intent is going to be embedded, try to forward the embedding TaskFragment // and its task id to embed back the original intent. if (taskFragment != null) { - ActivityOptions activityOptions = bOptions != null - ? ActivityOptions.fromBundle(bOptions) - : ActivityOptions.makeBasic(); activityOptions.setLaunchTaskFragmentToken(taskFragment.getFragmentToken()); - bOptions = activityOptions.toBundle(); } final IIntentSender target = mService.getIntentSenderLocked( INTENT_SENDER_ACTIVITY, mCallingPackage, mCallingFeatureId, callingUid, mUserId, null /*token*/, null /*resultCode*/, 0 /*requestCode*/, new Intent[] { mIntent }, new String[] { mResolvedType }, - flags, bOptions); + flags, activityOptions.toBundle()); return new IntentSender(target); } @@ -272,12 +271,12 @@ class ActivityStartInterceptor { * * @return the activity option used to start the original intent. */ - private Bundle deferCrossProfileAppsAnimationIfNecessary() { + private ActivityOptions deferCrossProfileAppsAnimationIfNecessary() { if (hasCrossProfileAnimation()) { mActivityOptions = null; - return ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(); + return ActivityOptions.makeOpenCrossProfileAppsAnimation(); } - return null; + return ActivityOptions.makeBasic(); } private boolean interceptQuietProfileIfNeeded() { diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 13f71521c240..f6d77ea33598 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -60,6 +60,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TAS import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; +import static com.android.server.pm.PackageArchiver.isArchivingEnabled; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; @@ -104,7 +105,6 @@ import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.AuxiliaryResolveInfo; -import android.content.pm.Flags; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; @@ -1034,7 +1034,7 @@ class ActivityStarter { } if (err == ActivityManager.START_SUCCESS && aInfo == null) { - if (Flags.archiving()) { + if (isArchivingEnabled()) { PackageArchiver packageArchiver = mService .getPackageManagerInternalLocked() .getPackageArchiver(); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 9c9cf04de3cc..0f36d8eafbe4 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -18,6 +18,10 @@ package com.android.server.wm; import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; +import static android.app.ComponentOptions.BackgroundActivityStartMode; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; @@ -31,6 +35,7 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; +import static com.android.window.flags.Flags.balRequireOptInSameUid; import static com.android.window.flags.Flags.balShowToasts; import static com.android.window.flags.Flags.balShowToastsBlocked; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; @@ -94,9 +99,9 @@ public class BackgroundActivityStartController { public static final ActivityOptions ACTIVITY_OPTIONS_SYSTEM_DEFINED = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) + MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) .setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); + MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); private final ActivityTaskManagerService mService; @@ -240,6 +245,7 @@ public class BackgroundActivityStartController { private final WindowProcessController mRealCallerApp; private final boolean mIsCallForResult; private final ActivityOptions mCheckedOptions; + private final String mAutoOptInReason; private BalVerdict mResultForCaller; private BalVerdict mResultForRealCaller; @@ -263,28 +269,38 @@ public class BackgroundActivityStartController { mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid); mIsCallForResult = resultRecord != null; mCheckedOptions = checkedOptions; - if (balRequireOptInByPendingIntentCreator() // auto-opt in introduced with this feature - && (originatingPendingIntent == null // not a PendingIntent - || mIsCallForResult) // sent for result - ) { + @BackgroundActivityStartMode int callerBackgroundActivityStartMode = + checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode(); + @BackgroundActivityStartMode int realCallerBackgroundActivityStartMode = + checkedOptions.getPendingIntentBackgroundActivityStartMode(); + + if (balRequireOptInByPendingIntentCreator() && originatingPendingIntent == null) { + mAutoOptInReason = "notPendingIntent"; + } else if (balRequireOptInByPendingIntentCreator() && mIsCallForResult) { + mAutoOptInReason = "callForResult"; + } else if (callingUid == realCallingUid && !balRequireOptInSameUid()) { + mAutoOptInReason = "sameUid"; + } else { + mAutoOptInReason = null; + } + + if (mAutoOptInReason != null) { // grant BAL privileges unless explicitly opted out mBalAllowedByPiCreatorWithHardening = mBalAllowedByPiCreator = - checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED - ? BackgroundStartPrivileges.NONE - : BackgroundStartPrivileges.ALLOW_BAL; - mBalAllowedByPiSender = - checkedOptions.getPendingIntentBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED + callerBackgroundActivityStartMode == MODE_BACKGROUND_ACTIVITY_START_DENIED ? BackgroundStartPrivileges.NONE : BackgroundStartPrivileges.ALLOW_BAL; + mBalAllowedByPiSender = realCallerBackgroundActivityStartMode + == MODE_BACKGROUND_ACTIVITY_START_DENIED + ? BackgroundStartPrivileges.NONE + : BackgroundStartPrivileges.ALLOW_BAL; } else { // for PendingIntents we restrict BAL based on target_sdk mBalAllowedByPiCreatorWithHardening = getBackgroundStartPrivilegesAllowedByCreator( callingUid, callingPackage, checkedOptions); final BackgroundStartPrivileges mBalAllowedByPiCreatorWithoutHardening = - checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED + callerBackgroundActivityStartMode + == MODE_BACKGROUND_ACTIVITY_START_DENIED ? BackgroundStartPrivileges.NONE : BackgroundStartPrivileges.ALLOW_BAL; mBalAllowedByPiCreator = balRequireOptInByPendingIntentCreator() @@ -326,11 +342,11 @@ public class BackgroundActivityStartController { private BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCreator( int callingUid, String callingPackage, ActivityOptions checkedOptions) { switch (checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()) { - case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: return BackgroundStartPrivileges.ALLOW_BAL; - case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED: + case MODE_BACKGROUND_ACTIVITY_START_DENIED: return BackgroundStartPrivileges.NONE; - case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: + case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: // no explicit choice by the app - let us decide what to do if (callingPackage != null) { // determine based on the calling/creating package @@ -431,6 +447,7 @@ public class BackgroundActivityStartController { sb.append("; hasRealCaller: ").append(hasRealCaller()); sb.append("; isCallForResult: ").append(mIsCallForResult); sb.append("; isPendingIntent: ").append(isPendingIntent()); + sb.append("; autoOptInReason: ").append(mAutoOptInReason); if (hasRealCaller()) { sb.append("; realCallingPackage: ") .append(getDebugPackageName(mRealCallingPackage, mRealCallingUid)); @@ -456,6 +473,50 @@ public class BackgroundActivityStartController { sb.append("]"); return sb.toString(); } + + public boolean isPendingIntentBalAllowedByPermission() { + return PendingIntentRecord.isPendingIntentBalAllowedByPermission(mCheckedOptions); + } + + public boolean callerExplicitOptInOrAutoOptIn() { + if (mAutoOptInReason == null) { + return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOWED; + } else { + return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + != MODE_BACKGROUND_ACTIVITY_START_DENIED; + } + } + + public boolean realCallerExplicitOptInOrAutoOptIn() { + if (mAutoOptInReason == null) { + return mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOWED; + } else { + return mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + != MODE_BACKGROUND_ACTIVITY_START_DENIED; + } + } + + public boolean callerExplicitOptOut() { + return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_DENIED; + } + + public boolean realCallerExplicitOptOut() { + return mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_DENIED; + } + + public boolean callerExplicitOptInOrOut() { + return mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; + } + + public boolean realCallerExplicitOptInOrOut() { + return mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; + } } static class BalVerdict { @@ -624,7 +685,7 @@ public class BackgroundActivityStartController { // PendingIntents is null). BalVerdict resultForRealCaller = state.callerIsRealCaller() && resultForCaller.allows() ? resultForCaller - : checkBackgroundActivityStartAllowedBySender(state, checkedOptions) + : checkBackgroundActivityStartAllowedBySender(state) .setBasedOnRealCaller(); state.setResultForRealCaller(resultForRealCaller); @@ -634,18 +695,14 @@ public class BackgroundActivityStartController { } // Handle cases with explicit opt-in - if (resultForCaller.allows() - && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) { + if (resultForCaller.allows() && state.callerExplicitOptInOrAutoOptIn()) { if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Activity start explicitly allowed by caller. " + state.dump()); } return allowBasedOnCaller(state); } - if (resultForRealCaller.allows() - && checkedOptions.getPendingIntentBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) { + if (resultForRealCaller.allows() && state.realCallerExplicitOptInOrAutoOptIn()) { if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "Activity start explicitly allowed by real caller. " + state.dump()); @@ -653,12 +710,9 @@ public class BackgroundActivityStartController { return allowBasedOnRealCaller(state); } // Handle PendingIntent cases with default behavior next - boolean callerCanAllow = resultForCaller.allows() - && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; + boolean callerCanAllow = resultForCaller.allows() && !state.callerExplicitOptOut(); boolean realCallerCanAllow = resultForRealCaller.allows() - && checkedOptions.getPendingIntentBackgroundActivityStartMode() - == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; + && !state.realCallerExplicitOptOut(); if (callerCanAllow && realCallerCanAllow) { // Both caller and real caller allow with system defined behavior if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { @@ -880,11 +934,9 @@ public class BackgroundActivityStartController { * @return A code denoting which BAL rule allows an activity to be started, * or {@link #BAL_BLOCK} if the launch should be blocked */ - BalVerdict checkBackgroundActivityStartAllowedBySender( - BalState state, - ActivityOptions checkedOptions) { + BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) { - if (PendingIntentRecord.isPendingIntentBalAllowedByPermission(checkedOptions) + if (state.isPendingIntentBalAllowedByPermission() && ActivityManager.checkComponentPermission( android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, state.mRealCallingUid, NO_PROCESS_UID, true) == PackageManager.PERMISSION_GRANTED) { @@ -1545,13 +1597,11 @@ public class BackgroundActivityStartController { state.mRealCallingUid, state.mResultForCaller == null ? BAL_BLOCK : state.mResultForCaller.getRawCode(), state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts(), - state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() - != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED, + state.callerExplicitOptInOrOut(), state.mResultForRealCaller == null ? BAL_BLOCK : state.mResultForRealCaller.getRawCode(), state.mBalAllowedByPiSender.allowsBackgroundActivityStarts(), - state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() - != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED + state.realCallerExplicitOptInOrOut() ); } diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java new file mode 100644 index 000000000000..5f488b769885 --- /dev/null +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -0,0 +1,284 @@ +/* + * 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.server.wm; + +import static android.content.Context.MEDIA_PROJECTION_SERVICE; + +import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR; + +import android.media.projection.IMediaProjectionManager; +import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionInfo; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.ContentRecordingSession; +import android.window.IScreenRecordingCallback; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; +import java.util.Map; +import java.util.Set; + +public class ScreenRecordingCallbackController { + + private final class Callback implements IBinder.DeathRecipient { + + IScreenRecordingCallback mCallback; + int mUid; + + Callback(IScreenRecordingCallback callback, int uid) { + this.mCallback = callback; + this.mUid = uid; + } + + public void binderDied() { + unregister(mCallback); + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private final Map<IBinder, Callback> mCallbacks = new ArrayMap<>(); + + @GuardedBy("WindowManagerService.mGlobalLock") + private final Map<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>(); + + private final WindowManagerService mWms; + + @GuardedBy("WindowManagerService.mGlobalLock") + private WindowContainer<WindowContainer> mRecordedWC; + + private boolean mWatcherCallbackRegistered = false; + + private final class MediaProjectionWatcherCallback extends + IMediaProjectionWatcherCallback.Stub { + @Override + public void onStart(MediaProjectionInfo mediaProjectionInfo) { + onScreenRecordingStart(mediaProjectionInfo); + } + + @Override + public void onStop(MediaProjectionInfo mediaProjectionInfo) { + onScreenRecordingStop(); + } + + @Override + public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo, + ContentRecordingSession contentRecordingSession) { + } + } + + ScreenRecordingCallbackController(WindowManagerService wms) { + mWms = wms; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void setRecordedWindowContainer(MediaProjectionInfo mediaProjectionInfo) { + if (mediaProjectionInfo.getLaunchCookie() == null) { + mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay(); + } else { + mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie + == mediaProjectionInfo.getLaunchCookie()).getTask(); + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void ensureMediaProjectionWatcherCallbackRegistered() { + if (mWatcherCallbackRegistered) { + return; + } + + IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + IMediaProjectionManager mediaProjectionManager = + IMediaProjectionManager.Stub.asInterface(binder); + + long identityToken = Binder.clearCallingIdentity(); + MediaProjectionInfo mediaProjectionInfo = null; + try { + mediaProjectionInfo = mediaProjectionManager.addCallback( + new MediaProjectionWatcherCallback()); + mWatcherCallbackRegistered = true; + } catch (RemoteException e) { + ProtoLog.e(WM_ERROR, "Failed to register MediaProjectionWatcherCallback"); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (mediaProjectionInfo != null) { + setRecordedWindowContainer(mediaProjectionInfo); + } + } + + boolean register(IScreenRecordingCallback callback) { + synchronized (mWms.mGlobalLock) { + ensureMediaProjectionWatcherCallbackRegistered(); + + IBinder binder = callback.asBinder(); + int uid = Binder.getCallingUid(); + + if (mCallbacks.containsKey(binder)) { + return mLastInvokedStateByUid.get(uid); + } + + Callback callbackInfo = new Callback(callback, uid); + try { + binder.linkToDeath(callbackInfo, 0); + } catch (RemoteException e) { + return false; + } + + boolean uidInRecording = uidHasRecordedActivity(callbackInfo.mUid); + mLastInvokedStateByUid.put(callbackInfo.mUid, uidInRecording); + mCallbacks.put(binder, callbackInfo); + return uidInRecording; + } + } + + void unregister(IScreenRecordingCallback callback) { + synchronized (mWms.mGlobalLock) { + IBinder binder = callback.asBinder(); + Callback callbackInfo = mCallbacks.remove(binder); + binder.unlinkToDeath(callbackInfo, 0); + + boolean uidHasCallback = false; + for (Callback cb : mCallbacks.values()) { + if (cb.mUid == callbackInfo.mUid) { + uidHasCallback = true; + break; + } + } + if (!uidHasCallback) { + mLastInvokedStateByUid.remove(callbackInfo.mUid); + } + } + } + + private void onScreenRecordingStart(MediaProjectionInfo mediaProjectionInfo) { + synchronized (mWms.mGlobalLock) { + setRecordedWindowContainer(mediaProjectionInfo); + dispatchCallbacks(getRecordedUids(), true /* visibleInScreenRecording*/); + } + } + + private void onScreenRecordingStop() { + synchronized (mWms.mGlobalLock) { + dispatchCallbacks(getRecordedUids(), false /*visibleInScreenRecording*/); + mRecordedWC = null; + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + void onProcessActivityVisibilityChanged(int uid, boolean processVisible) { + // If recording isn't active or there's no registered callback for the uid, there's nothing + // to do on this visibility change. + if (mRecordedWC == null || !mLastInvokedStateByUid.containsKey(uid)) { + return; + } + + // If the callbacks are already in the correct state, avoid making duplicate callbacks for + // the same state. This can happen when: + // * a process becomes visible but its UID already has a recorded activity from another + // process. + // * a process becomes invisible but its UID already doesn't have any recorded activities. + if (processVisible == mLastInvokedStateByUid.get(uid)) { + return; + } + + // If the process visibility change doesn't change the visibility of the UID, avoid making + // duplicate callbacks for the same state. This can happen when: + // * a process becomes visible but the newly visible activity isn't in the recorded window + // container. + // * a process becomes invisible but there are still activities being recorded for the UID. + boolean uidInRecording = uidHasRecordedActivity(uid); + if ((processVisible && !uidInRecording) || (!processVisible && uidInRecording)) { + return; + } + + dispatchCallbacks(Set.of(uid), processVisible); + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private boolean uidHasRecordedActivity(int uid) { + if (mRecordedWC == null) { + return false; + } + boolean[] hasRecordedActivity = {false}; + mRecordedWC.forAllActivities(activityRecord -> { + if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested()) { + hasRecordedActivity[0] = true; + return true; + } + return false; + }, true /*traverseTopToBottom*/); + return hasRecordedActivity[0]; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private Set<Integer> getRecordedUids() { + Set<Integer> result = new ArraySet<>(); + if (mRecordedWC == null) { + return result; + } + mRecordedWC.forAllActivities(activityRecord -> { + if (activityRecord.isVisibleRequested() && mLastInvokedStateByUid.containsKey( + activityRecord.getUid())) { + result.add(activityRecord.getUid()); + } + }, true /*traverseTopToBottom*/); + return result; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void dispatchCallbacks(Set<Integer> uids, boolean visibleInScreenRecording) { + if (uids.isEmpty()) { + return; + } + + for (Integer uid : uids) { + mLastInvokedStateByUid.put(uid, visibleInScreenRecording); + } + + for (Callback callback : mCallbacks.values()) { + if (!uids.contains(callback.mUid)) { + continue; + } + try { + callback.mCallback.onScreenRecordingStateChanged(visibleInScreenRecording); + } catch (RemoteException e) { + // Client has died. Cleanup is handled via DeathRecipient. + } + } + } + + void dump(PrintWriter pw) { + pw.format("ScreenRecordingCallbackController:\n"); + pw.format(" Registered callbacks:\n"); + for (Map.Entry<IBinder, Callback> entry : mCallbacks.entrySet()) { + pw.format(" callback=%s uid=%s\n", entry.getKey(), entry.getValue().mUid); + } + pw.format(" Last invoked states:\n"); + for (Map.Entry<Integer, Boolean> entry : mLastInvokedStateByUid.entrySet()) { + pw.format(" uid=%s isVisibleInScreenRecording=%s\n", entry.getKey(), + entry.getValue()); + } + } +} diff --git a/services/core/java/com/android/server/wm/SensitiveContentPackages.java b/services/core/java/com/android/server/wm/SensitiveContentPackages.java index 3862b82512c3..a7d6903bbe30 100644 --- a/services/core/java/com/android/server/wm/SensitiveContentPackages.java +++ b/services/core/java/com/android/server/wm/SensitiveContentPackages.java @@ -21,7 +21,6 @@ import android.util.ArraySet; import java.io.PrintWriter; import java.util.Objects; -import java.util.Set; /** * Cache of distinct package/uid pairs that require being blocked from screen capture. This class is @@ -41,10 +40,33 @@ public class SensitiveContentPackages { return false; } - /** Replaces the set of package/uid pairs to set that should be blocked from screen capture */ - public void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos) { - mProtectedPackages.clear(); + /** + * Adds the set of package/uid pairs to set that should be blocked from screen capture + * + * @param packageInfos packages to be blocked + * @return {@code true} if packages set is modified, {@code false} otherwise. + */ + public boolean addBlockScreenCaptureForApps(@NonNull ArraySet<PackageInfo> packageInfos) { + if (mProtectedPackages.equals(packageInfos)) { + // new set is equal to current set of packages, no need to update + return false; + } mProtectedPackages.addAll(packageInfos); + return true; + } + + /** + * Clears the set of package/uid pairs that should be blocked from screen capture + * + * @return {@code true} if packages set is modified, {@code false} otherwise. + */ + public boolean clearBlockedApps() { + if (mProtectedPackages.isEmpty()) { + // set was already empty + return false; + } + mProtectedPackages.clear(); + return true; } void dump(PrintWriter pw) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 314d720692f7..8f9ed8353456 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3558,9 +3558,11 @@ class Task extends TaskFragment { && top.mLetterboxUiController.isSystemOverrideToFullscreenEnabled(); appCompatTaskInfo.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap(); - if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + if (top != null) { appCompatTaskInfo.topActivityLetterboxWidth = top.getBounds().width(); appCompatTaskInfo.topActivityLetterboxHeight = top.getBounds().height(); + } + if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) { if (appCompatTaskInfo.isTopActivityPillarboxed()) { // Pillarboxed appCompatTaskInfo.topActivityLetterboxHorizontalPosition = diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f620a9743eb4..2accf9a2a43a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2841,6 +2841,19 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** Returns {@code true} if the display should use high performance hint for this transition. */ + boolean shouldUsePerfHint(@NonNull DisplayContent dc) { + if (mOverrideOptions != null + && mOverrideOptions.getType() == ActivityOptions.ANIM_SCENE_TRANSITION + && mType == TRANSIT_TO_BACK && mParticipants.size() == 1) { + // This should be from convertFromTranslucent that makes the occluded activity invisible + // without animation. So do not use perf hint (especially early-wakeup) that may disturb + // SurfaceFlinger scheduling around the last frame. + return false; + } + return mTargetDisplays.contains(dc); + } + /** * Returns {@code true} if the transition and the corresponding transaction should be applied * on display thread. Currently, this only checks for display rotation change because the order diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 708d63e27ec2..59e3350d5c13 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -1237,8 +1237,15 @@ class TransitionController { // enableHighPerfTransition(true) is also called in Transition#recordDisplay. for (int i = mAtm.mRootWindowContainer.getChildCount() - 1; i >= 0; i--) { final DisplayContent dc = mAtm.mRootWindowContainer.getChildAt(i); - if (isTransitionOnDisplay(dc)) { + if (mCollectingTransition != null && mCollectingTransition.shouldUsePerfHint(dc)) { dc.enableHighPerfTransition(true); + continue; + } + for (int j = mPlayingTransitions.size() - 1; j >= 0; j--) { + if (mPlayingTransitions.get(j).shouldUsePerfHint(dc)) { + dc.enableHighPerfTransition(true); + break; + } } } // Usually transitions put quite a load onto the system already (with all the things diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 22b690e85c35..f2a58e54bfbe 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -32,6 +32,7 @@ import android.hardware.display.VirtualDisplayConfig; import android.os.Bundle; import android.os.IBinder; import android.os.Message; +import android.util.ArraySet; import android.util.Pair; import android.view.ContentRecordingSession; import android.view.Display; @@ -1020,5 +1021,13 @@ public abstract class WindowManagerInternal { * * @param packageInfos set of {@link PackageInfo} whose windows should be blocked from capture */ - public abstract void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos); + public abstract void addBlockScreenCaptureForApps(@NonNull ArraySet<PackageInfo> packageInfos); + + /** + * Clears apps added to collection of apps in which screen capture should be disabled. + * + * <p> This clears and resets any existing set or added applications from + * * {@link #addBlockScreenCaptureForApps(ArraySet)} + */ + public abstract void clearBlockedApps(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b38dc008f4e5..6c833565119f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,6 +303,7 @@ import android.view.displayhash.VerifiedDisplayHash; import android.view.inputmethod.ImeTracker; import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; +import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ITrustedPresentationListener; @@ -369,7 +370,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -1104,6 +1104,8 @@ public class WindowManagerService extends IWindowManager.Stub void onAppFreezeTimeout(); } + private final ScreenRecordingCallbackController mScreenRecordingCallbackController; + public static WindowManagerService main(final Context context, final InputManagerService im, final boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm) { @@ -1345,6 +1347,7 @@ public class WindowManagerService extends IWindowManager.Stub mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); mAccessibilityController = new AccessibilityController(this); + mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this); mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> { synchronized (mGlobalLock) { DisplayContent dc = mRoot.getDisplayContent(displayId); @@ -7188,6 +7191,7 @@ public class WindowManagerService extends IWindowManager.Stub mSystemPerformanceHinter.dump(pw, ""); mTrustedPresentationListenerController.dump(pw); mSensitiveContentPackages.dump(pw); + mScreenRecordingCallbackController.dump(pw); } } @@ -8571,10 +8575,23 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void setShouldBlockScreenCaptureForApp(Set<PackageInfo> packageInfos) { + public void addBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) { + synchronized (mGlobalLock) { + boolean modified = + mSensitiveContentPackages.addBlockScreenCaptureForApps(packageInfos); + if (modified) { + WindowManagerService.this.refreshScreenCaptureDisabled(); + } + } + } + + @Override + public void clearBlockedApps() { synchronized (mGlobalLock) { - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(packageInfos); - WindowManagerService.this.refreshScreenCaptureDisabled(); + boolean modified = mSensitiveContentPackages.clearBlockedApps(); + if (modified) { + WindowManagerService.this.refreshScreenCaptureDisabled(); + } } } } @@ -9889,4 +9906,18 @@ public class WindowManagerService extends IWindowManager.Stub int id) { mTrustedPresentationListenerController.unregisterListener(listener, id); } + + @Override + public boolean registerScreenRecordingCallback(IScreenRecordingCallback callback) { + return mScreenRecordingCallbackController.register(callback); + } + + @Override + public void unregisterScreenRecordingCallback(IScreenRecordingCallback callback) { + mScreenRecordingCallbackController.unregister(callback); + } + + void onProcessActivityVisibilityChanged(int uid, boolean visible) { + mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible); + } } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index b8fa5e5b2786..6d2e8cc29506 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -1271,8 +1271,10 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; if (!wasAnyVisible && anyVisible) { mAtm.mVisibleActivityProcessTracker.onAnyActivityVisible(this); + mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, true /*visible*/); } else if (wasAnyVisible && !anyVisible) { mAtm.mVisibleActivityProcessTracker.onAllActivitiesInvisible(this); + mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, false /*visible*/); } else if (wasAnyVisible && !wasResumed && hasResumedActivity()) { mAtm.mVisibleActivityProcessTracker.onActivityResumedWhileVisible(this); } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 775570cb08ba..dfa9dcecfbb5 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -37,7 +37,6 @@ cc_library_static { "com_android_server_adb_AdbDebuggingManager.cpp", "com_android_server_am_BatteryStatsService.cpp", "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp", - "com_android_server_BootReceiver.cpp", "com_android_server_ConsumerIrService.cpp", "com_android_server_companion_virtual_InputController.cpp", "com_android_server_devicepolicy_CryptoTestHelper.cpp", @@ -95,16 +94,6 @@ cc_library_static { header_libs: [ "bionic_libc_platform_headers", ], - - static_libs: [ - "libunwindstack", - ], - - whole_static_libs: [ - "libdebuggerd_tombstone_proto_to_text", - ], - - runtime_libs: ["libdexfile"], } cc_defaults { diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index 15eb7c64a40c..cc08488742b2 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -33,7 +33,3 @@ per-file com_android_server_companion_virtual_InputController.cpp = file:/servic # Bug component : 158088 = per-file *AnrTimer* per-file *AnrTimer* = file:/PERFORMANCE_OWNERS - -# Bug component : 158088 = per-file com_android_server_utils_AnrTimer*.java -per-file com_android_server_utils_AnrTimer*.java = file:/PERFORMANCE_OWNERS -per-file com_android_server_BootReceiver.cpp = file:/STABILITY_OWNERS diff --git a/services/core/jni/com_android_server_BootReceiver.cpp b/services/core/jni/com_android_server_BootReceiver.cpp deleted file mode 100644 index 3892d284dafb..000000000000 --- a/services/core/jni/com_android_server_BootReceiver.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <libdebuggerd/tombstone.h> -#include <nativehelper/JNIHelp.h> - -#include <sstream> - -#include "jni.h" -#include "tombstone.pb.h" - -namespace android { - -static void writeToString(std::stringstream& ss, const std::string& line, bool should_log) { - ss << line << std::endl; -} - -static jstring com_android_server_BootReceiver_getTombstoneText(JNIEnv* env, jobject, - jbyteArray tombstoneBytes) { - Tombstone tombstone; - tombstone.ParseFromArray(env->GetByteArrayElements(tombstoneBytes, 0), - env->GetArrayLength(tombstoneBytes)); - - std::stringstream tombstoneString; - - tombstone_proto_to_text(tombstone, - std::bind(&writeToString, std::ref(tombstoneString), - std::placeholders::_1, std::placeholders::_2)); - - return env->NewStringUTF(tombstoneString.str().c_str()); -} - -static const JNINativeMethod sMethods[] = { - /* name, signature, funcPtr */ - {"getTombstoneText", "([B)Ljava/lang/String;", - (jstring*)com_android_server_BootReceiver_getTombstoneText}, -}; - -int register_com_android_server_BootReceiver(JNIEnv* env) { - return jniRegisterNativeMethods(env, "com/android/server/BootReceiver", sMethods, - NELEM(sMethods)); -} - -} // namespace android diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 414339d3f349..2b9bb7a4de87 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -685,6 +685,8 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon { // acquire lock std::scoped_lock _l(mLock); + outConfig->mousePointerSpeed = mLocked.pointerSpeed; + outConfig->mousePointerAccelerationEnabled = mLocked.mousePointerAccelerationEnabled; outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerVelocityControlParameters.acceleration = diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index a4b1f841d3bc..5d1eb496903b 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -66,7 +66,6 @@ int register_android_server_stats_pull_StatsPullAtomService(JNIEnv* env); int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env); int register_android_server_companion_virtual_InputController(JNIEnv* env); int register_android_server_app_GameManagerService(JNIEnv* env); -int register_com_android_server_BootReceiver(JNIEnv* env); int register_com_android_server_wm_TaskFpsCallbackController(JNIEnv* env); int register_com_android_server_display_DisplayControl(JNIEnv* env); int register_com_android_server_SystemClockTime(JNIEnv* env); @@ -129,7 +128,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_sensor_SensorService(vm, env); register_android_server_companion_virtual_InputController(env); register_android_server_app_GameManagerService(env); - register_com_android_server_BootReceiver(env); register_com_android_server_wm_TaskFpsCallbackController(env); register_com_android_server_display_DisplayControl(env); register_com_android_server_SystemClockTime(env); diff --git a/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt index 94caf2865b66..c8a65459d3df 100644 --- a/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt @@ -17,6 +17,7 @@ package com.android.server.permission.access.appop import android.app.AppOpsManager +import android.util.Slog import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.MutableAccessState import com.android.server.permission.access.MutateStateScope @@ -84,6 +85,10 @@ class AppIdAppOpPolicy : BaseAppOpPolicy(AppIdAppOpPersistence()) { appOpName: String, mode: Int ): Boolean { + if (userId !in newState.userStates) { + Slog.e(LOG_TAG, "Unable to set app op mode for missing user $userId") + return false + } val defaultMode = AppOpsManager.opToDefaultMode(appOpName) val oldMode = newState.userStates[userId]!! @@ -152,4 +157,8 @@ class AppIdAppOpPolicy : BaseAppOpPolicy(AppIdAppOpPersistence()) { */ abstract fun onStateMutated() } + + companion object { + private val LOG_TAG = AppIdAppOpPolicy::class.java.simpleName + } } diff --git a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt index 0d9470edc4ea..2f15dc7b232a 100644 --- a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt @@ -17,6 +17,7 @@ package com.android.server.permission.access.appop import android.app.AppOpsManager +import android.util.Slog import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.MutableAccessState import com.android.server.permission.access.MutateStateScope @@ -87,6 +88,10 @@ class PackageAppOpPolicy : BaseAppOpPolicy(PackageAppOpPersistence()) { appOpName: String, mode: Int ): Boolean { + if (userId !in newState.userStates) { + Slog.e(LOG_TAG, "Unable to set app op mode for missing user $userId") + return false + } val defaultMode = AppOpsManager.opToDefaultMode(appOpName) val oldMode = newState.userStates[userId]!! @@ -155,4 +160,8 @@ class PackageAppOpPolicy : BaseAppOpPolicy(PackageAppOpPersistence()) { */ abstract fun onStateMutated() } + + companion object { + private val LOG_TAG = PackageAppOpPolicy::class.java.simpleName + } } diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 022268df4a63..62d2d7ee848a 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -134,7 +134,9 @@ class AppIdPermissionPolicy : SchemePolicy() { ) { val changedPermissionNames = MutableIndexedSet<String>() packageNames.forEachIndexed { _, packageName -> - val packageState = newState.externalState.packageStates[packageName]!! + // The package may still be removed even if it was once notified as installed. + val packageState = newState.externalState.packageStates[packageName] + ?: return@forEachIndexed adoptPermissions(packageState, changedPermissionNames) addPermissionGroups(packageState) addPermissions(packageState, changedPermissionNames) @@ -147,12 +149,14 @@ class AppIdPermissionPolicy : SchemePolicy() { } packageNames.forEachIndexed { _, packageName -> - val packageState = newState.externalState.packageStates[packageName]!! + val packageState = newState.externalState.packageStates[packageName] + ?: return@forEachIndexed val installedPackageState = if (isSystemUpdated) packageState else null evaluateAllPermissionStatesForPackage(packageState, installedPackageState) } packageNames.forEachIndexed { _, packageName -> - val packageState = newState.externalState.packageStates[packageName]!! + val packageState = newState.externalState.packageStates[packageName] + ?: return@forEachIndexed newState.externalState.userIds.forEachIndexed { _, userId -> inheritImplicitPermissionStates(packageState.appId, userId) } @@ -1607,6 +1611,13 @@ class AppIdPermissionPolicy : SchemePolicy() { flagMask: Int, flagValues: Int ): Boolean { + if (userId !in newState.userStates) { + // Despite that we check UserManagerInternal.exists() in PermissionService, we may still + // sometimes get race conditions between that check and the actual mutateState() call. + // This should rarely happen but at least we should not crash. + Slog.e(LOG_TAG, "Unable to update permission flags for missing user $userId") + return false + } val oldFlags = newState.userStates[userId]!! .appIdPermissionFlags[appId] diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 83479e28fe24..dd8c6a23b829 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -295,6 +295,7 @@ public class DisplayModeDirectorTest { private static final float HBM_TRANSITION_POINT_INVALID = Float.POSITIVE_INFINITY; private Context mContext; + private Resources mResources; private FakesInjector mInjector; private Handler mHandler; @Rule @@ -318,6 +319,8 @@ public class DisplayModeDirectorTest { @Before public void setUp() throws Exception { mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + mResources = mockResources(); + when(mContext.getResources()).thenReturn(mResources); final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext); when(mContext.getContentResolver()).thenReturn(resolver); mInjector = spy(new FakesInjector(mDisplayManagerInternalMock, mStatusBarMock, @@ -325,6 +328,60 @@ public class DisplayModeDirectorTest { mHandler = new Handler(Looper.getMainLooper()); } + private Resources mockResources() { + var resources = mock(Resources.class); + when(resources.getBoolean(R.bool.config_ignoreUdfpsVote)) + .thenReturn(false); + when(resources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled)) + .thenReturn(false); + when(resources.getBoolean(R.bool.config_supportsDvrr)) + .thenReturn(false); + when(resources.getInteger(R.integer.config_displayWhiteBalanceBrightnessFilterHorizon)) + .thenReturn(10000); + when(resources.getInteger(R.integer.config_defaultPeakRefreshRate)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_externalDisplayPeakWidth)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_externalDisplayPeakHeight)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_defaultRefreshRateInZone)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_defaultRefreshRate)) + .thenReturn(60); + when(resources.getInteger(R.integer.config_defaultRefreshRateInHbmHdr)) + .thenReturn(0); + when(resources.getInteger(R.integer.config_defaultRefreshRateInHbmSunlight)) + .thenReturn(0); + + when(resources.getString(R.string.config_displayLightSensorType)) + .thenReturn(null); + + when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) + .thenReturn(new int[]{}); + when(resources.getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(new int[]{}); + when(resources.getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(new int[]{}); + when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) + .thenReturn(new int[]{}); + + doAnswer(invocation -> { + TypedValue value = invocation.getArgument(1); + value.type = TypedValue.TYPE_FLOAT; + value.data = Float.floatToIntBits(10f); + return null; // void method, so return null + }).when(resources).getValue(eq(R.dimen.config_displayWhiteBalanceBrightnessFilterIntercept), + any(), eq(true)); + + return resources; + } + private DisplayModeDirector createDirectorFromRefreshRateArray( float[] refreshRates, int baseModeId) { return createDirectorFromRefreshRateArray(refreshRates, baseModeId, refreshRates[0]); @@ -2911,31 +2968,31 @@ public class DisplayModeDirectorTest { @Test public void testNotifyDefaultDisplayDeviceUpdated() { - Resources resources = mock(Resources.class); - when(mContext.getResources()).thenReturn(resources); - when(resources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate)) + when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate)) .thenReturn(75); - when(resources.getInteger(R.integer.config_defaultRefreshRate)) + when(mResources.getInteger(R.integer.config_defaultRefreshRate)) .thenReturn(45); - when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) + when(mResources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) .thenReturn(65); - when(resources.getInteger(R.integer.config_defaultRefreshRateInZone)) + when(mResources.getInteger(R.integer.config_defaultRefreshRateInZone)) .thenReturn(85); - when(resources.getInteger(R.integer.config_defaultRefreshRateInHbmHdr)) + when(mResources.getInteger(R.integer.config_defaultRefreshRateInHbmHdr)) .thenReturn(95); - when(resources.getInteger(R.integer.config_defaultRefreshRateInHbmSunlight)) + when(mResources.getInteger(R.integer.config_defaultRefreshRateInHbmSunlight)) .thenReturn(100); - when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) + when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) .thenReturn(new int[]{5}); - when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) + when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) .thenReturn(new int[]{10}); when( - resources.getIntArray(R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate)) + mResources.getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate)) .thenReturn(new int[]{250}); when( - resources.getIntArray(R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) + mResources.getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) .thenReturn(new int[]{7000}); - when(resources.getInteger( + when(mResources.getInteger( com.android.internal.R.integer.config_displayWhiteBalanceBrightnessFilterHorizon)) .thenReturn(3); ArgumentCaptor<TypedValue> valueArgumentCaptor = ArgumentCaptor.forClass(TypedValue.class); @@ -2943,7 +3000,7 @@ public class DisplayModeDirectorTest { valueArgumentCaptor.getValue().type = 4; valueArgumentCaptor.getValue().data = 13; return null; - }).when(resources).getValue(eq(com.android.internal.R.dimen + }).when(mResources).getValue(eq(com.android.internal.R.dimen .config_displayWhiteBalanceBrightnessFilterIntercept), valueArgumentCaptor.capture(), eq(true)); DisplayModeDirector director = diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java index ff91d34470d4..92016dfc631b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java @@ -20,11 +20,10 @@ package com.android.server.display.mode; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.Mode.INVALID_MODE_ID; - import static com.android.server.display.mode.DisplayModeDirector.SYNCHRONIZED_REFRESH_RATE_TOLERANCE; import static com.android.server.display.mode.Vote.PRIORITY_LIMIT_MODE; -import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE; import static com.android.server.display.mode.Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE; +import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE; import static com.android.server.display.mode.VotesStorage.GLOBAL_ID; import static com.google.common.truth.Truth.assertThat; @@ -43,6 +42,7 @@ import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.Looper; import android.provider.DeviceConfigInterface; +import android.test.mock.MockContentResolver; import android.view.Display; import android.view.DisplayInfo; @@ -51,21 +51,26 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.sensors.SensorManagerInternal; +import junitparams.JUnitParamsRunner; + 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 junitparams.JUnitParamsRunner; - - @SmallTest @RunWith(JUnitParamsRunner.class) public class DisplayObserverTest { + @Rule + public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + private static final int EXTERNAL_DISPLAY = 1; private static final int MAX_WIDTH = 1920; private static final int MAX_HEIGHT = 1080; @@ -120,6 +125,8 @@ public class DisplayObserverTest { mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); mResources = mock(Resources.class); when(mContext.getResources()).thenReturn(mResources); + MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext); + when(mContext.getContentResolver()).thenReturn(resolver); when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) .thenReturn(0); when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java index b363fd4cc7cb..d78143381ae5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java @@ -20,11 +20,13 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.media.projection.MediaProjectionInfo; @@ -35,6 +37,7 @@ import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper.RunWithLooper; +import android.util.ArraySet; import androidx.test.filters.SmallTest; @@ -52,7 +55,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import java.util.Collections; import java.util.Set; @SmallTest @@ -68,6 +70,8 @@ public class SensitiveContentProtectionManagerServiceTest { private static final int NOTIFICATION_UID_1 = 5; private static final int NOTIFICATION_UID_2 = 6; + private static final ArraySet<PackageInfo> EMPTY_SET = new ArraySet<>(); + @Rule public final TestableContext mContext = new TestableContext(getInstrumentation().getTargetContext(), null); @@ -107,6 +111,9 @@ public class SensitiveContentProtectionManagerServiceTest { mSensitiveContentProtectionManagerService.mNotificationListener = spy(mSensitiveContentProtectionManagerService.mNotificationListener); + doCallRealMethod() + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .onListenerConnected(); // Setup RankingMap and two possilbe rankings when(mSensitiveRanking.hasSensitiveContent()).thenReturn(true); @@ -128,7 +135,7 @@ public class SensitiveContentProtectionManagerServiceTest { mSensitiveContentProtectionManagerService.onDestroy(); } - private Set<PackageInfo> setupSensitiveNotification() { + private ArraySet<PackageInfo> setupSensitiveNotification() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -149,10 +156,11 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mNonSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1))); } - private Set<PackageInfo> setupMultipleSensitiveNotificationsFromSamePackageAndUid() { + private ArraySet<PackageInfo> setupMultipleSensitiveNotificationsFromSamePackageAndUid() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -173,10 +181,11 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1))); } - private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentPackage() { + private ArraySet<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentPackage() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -197,11 +206,12 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), - new PackageInfo(NOTIFICATION_PKG_2, NOTIFICATION_UID_1)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), + new PackageInfo(NOTIFICATION_PKG_2, NOTIFICATION_UID_1))); } - private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentUid() { + private ArraySet<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentUid() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -222,8 +232,9 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), - new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_2)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), + new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_2))); } private void setupNoSensitiveNotifications() { @@ -251,11 +262,11 @@ public class SensitiveContentProtectionManagerServiceTest { @Test public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test @@ -264,7 +275,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -273,37 +284,37 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test public void mediaProjectionOnStart_multipleNotifications_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = + ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromSamePackageAndUid(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test public void mediaProjectionOnStart_multiplePackages_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = + ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromDifferentPackage(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test public void mediaProjectionOnStart_multipleUid_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = + ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromDifferentUid(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test @@ -316,12 +327,12 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).clearBlockedApps(); } @Test public void mediaProjectionOnStart_afterOnStop_onProjectionStart_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); @@ -330,7 +341,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test @@ -341,7 +352,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -352,7 +363,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -363,7 +374,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -376,6 +387,314 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_projectionNotStarted_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnListenerConnected_projectionStopped_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnListenerConnected_projectionStarted_setWmBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); + } + + @Test + public void nlsOnListenerConnected_noSensitiveNotifications_noBlockedPackages() { + setupNoSensitiveNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_noNotifications_noBlockedPackages() { + setupNoNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_nullRankingMap_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + doReturn(null) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_missingRanking_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + doReturn(mRankingMap) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_projectionNotStarted_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationRankingUpdate_projectionStopped_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationRankingUpdate_projectionStarted_setWmBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); + } + + @Test + public void nlsOnNotificationRankingUpdate_noSensitiveNotifications_noBlockedPackages() { + setupNoSensitiveNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_noNotifications_noBlockedPackages() { + setupNoNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_nullRankingMap_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(null); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_missingRanking_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + doReturn(mRankingMap) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_getActiveNotificationsThrows_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + doThrow(SecurityException.class) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationPosted_projectionNotStarted_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_projectionStopped_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_projectionStarted_setWmBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + ArraySet<PackageInfo> expectedBlockedPackages = new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1))); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); + } + + @Test + public void nlsOnNotificationPosted_noSensitiveNotifications_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification2, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_noNotifications_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(null, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_nullRankingMap_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, null); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_missingRanking_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java new file mode 100644 index 000000000000..7d3a1103a044 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.am.ActivityManagerService.Injector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.ApplicationStartInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.platform.test.annotations.Presubmit; +import android.text.TextUtils; + +import com.android.server.LocalServices; +import com.android.server.ServiceThread; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; + +/** + * Test class for {@link android.app.ApplicationStartInfo}. + * + * Build/Install/Run: + * atest ApplicationStartInfoTest + */ +@Presubmit +public class ApplicationStartInfoTest { + + private static final String TAG = ApplicationStartInfoTest.class.getSimpleName(); + private static final ComponentName COMPONENT = new ComponentName("com.android.test", ".Foo"); + + @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule(); + @Mock private AppOpsService mAppOpsService; + @Mock private PackageManagerInternal mPackageManagerInt; + + private Context mContext = getInstrumentation().getTargetContext(); + private TestInjector mInjector; + private ActivityManagerService mAms; + private ProcessList mProcessList; + private AppStartInfoTracker mAppStartInfoTracker; + private Handler mHandler; + private HandlerThread mHandlerThread; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + mProcessList = spy(new ProcessList()); + mAppStartInfoTracker = spy(new AppStartInfoTracker()); + mAppStartInfoTracker.mEnabled = true; + setFieldValue(ProcessList.class, mProcessList, "mAppStartInfoTracker", + mAppStartInfoTracker); + mInjector = new TestInjector(mContext); + mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread()); + mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal()); + mAms.mPackageManagerInt = mPackageManagerInt; + mAppStartInfoTracker.mService = mAms; + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + doReturn("com.android.test").when(mPackageManagerInt).getNameForUid(anyInt()); + // Remove stale instance of PackageManagerInternal if there is any + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + } + + @After + public void tearDown() { + mHandlerThread.quit(); + } + + @Test + public void testApplicationStartInfo() throws Exception { + mAppStartInfoTracker.clearProcessStartInfo(true); + mAppStartInfoTracker.mAppStartInfoLoaded.set(true); + mAppStartInfoTracker.mAppStartInfoHistoryListSize = + mAppStartInfoTracker.APP_START_INFO_HISTORY_LIST_SIZE; + mAppStartInfoTracker.mProcStartStoreDir = new File(mContext.getFilesDir(), + AppStartInfoTracker.APP_START_STORE_DIR); + assertTrue(FileUtils.createDir(mAppStartInfoTracker.mProcStartStoreDir)); + mAppStartInfoTracker.mProcStartInfoFile = new File(mAppStartInfoTracker.mProcStartStoreDir, + AppStartInfoTracker.APP_START_INFO_FILE); + + doNothing().when(mAppStartInfoTracker).schedulePersistProcessStartInfo(anyBoolean()); + + final int app1Uid = 10123; + final int app1Pid1 = 12345; + final int app1Pid2 = 12346; + final int app1DefiningUid = 23456; + final int app1UidUser2 = 1010123; + final int app1PidUser2 = 12347; + final String app1ProcessName = "com.android.test.stub1:process"; + final String app1PackageName = "com.android.test.stub1"; + final long appStartTimestampIntentStarted = 1000000; + final long appStartTimestampActivityLaunchFinished = 2000000; + final long appStartTimestampReportFullyDrawn = 3000000; + final long appStartTimestampService = 4000000; + final long appStartTimestampBroadcast = 5000000; + final long appStartTimestampRContentProvider = 6000000; + + ProcessRecord app = makeProcessRecord( + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + app1PackageName); // packageName + + ArrayList<ApplicationStartInfo> list = new ArrayList<ApplicationStartInfo>(); + + // Case 1: Activity start intent failed + mAppStartInfoTracker.onIntentStarted(buildIntent(COMPONENT), + appStartTimestampIntentStarted); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 0); + + verifyInProgApplicationStartInfo( + 0, // index + 0, // pid + 0, // uid + 0, // packageUid + null, // definingUid + null, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_UNSET, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onIntentFailed(appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(0); + assertEquals(list.size(), 0); + + mAppStartInfoTracker.clearProcessStartInfo(true); + + // Case 2: Activity start launch cancelled + mAppStartInfoTracker.onIntentStarted(buildIntent(COMPONENT), + appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 0); + + mAppStartInfoTracker.onActivityLaunched(appStartTimestampIntentStarted, COMPONENT, + ApplicationStartInfo.START_TYPE_COLD, app); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 1); + + verifyInProgApplicationStartInfo( + 0, // index + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onActivityLaunchCancelled(appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(0); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_ERROR, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.clearProcessStartInfo(true); + + // Case 3: Activity start success + mAppStartInfoTracker.onIntentStarted(buildIntent(COMPONENT), + appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 0); + + mAppStartInfoTracker.onActivityLaunched(appStartTimestampIntentStarted, COMPONENT, + ApplicationStartInfo.START_TYPE_COLD, app); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 1); + + verifyInProgApplicationStartInfo( + 0, // index + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onActivityLaunchFinished(appStartTimestampIntentStarted, COMPONENT, + appStartTimestampActivityLaunchFinished, ApplicationStartInfo.LAUNCH_MODE_STANDARD); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 1); + + verifyInProgApplicationStartInfo( + 0, // index + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onReportFullyDrawn(appStartTimestampIntentStarted, + appStartTimestampReportFullyDrawn); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(0); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Don't clear records for use in subsequent cases. + + // Case 4: Create an other app1 record with different pid started for a service + sleep(1); + app = makeProcessRecord( + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + app1PackageName); // packageName + ServiceRecord service = ServiceRecord.newEmptyInstanceForTest(mAms); + + mAppStartInfoTracker.handleProcessServiceStart(appStartTimestampService, app, service, + false); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, 0, 0, list); + assertEquals(list.size(), 2); + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_SERVICE, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_WARM, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Case 5: Create an instance of app1 with a different user started for a broadcast + sleep(1); + app = makeProcessRecord( + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + app1PackageName); // packageName + + mAppStartInfoTracker.handleProcessBroadcastStart(appStartTimestampBroadcast, app, + null, true /* isColdStart */); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1UidUser2, app1PidUser2, 0, list); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_BROADCAST, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Case 6: User 2 gets removed + mAppStartInfoTracker.onPackageRemoved(app1PackageName, app1UidUser2, false); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1UidUser2, app1PidUser2, 0, list); + assertEquals(list.size(), 0); + + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1PidUser2, 0, list); + assertEquals(list.size(), 2); + + + // Case 7: Create a process from another package started for a content provider + final int app2UidUser2 = 1010234; + final int app2PidUser2 = 12348; + final String app2ProcessName = "com.android.test.stub2:process"; + final String app2PackageName = "com.android.test.stub2"; + + sleep(1); + + app = makeProcessRecord( + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + app2ProcessName, // processName + app2PackageName); // packageName + + mAppStartInfoTracker.handleProcessContentProviderStart(appStartTimestampRContentProvider, + app, false); + list.clear(); + mAppStartInfoTracker.getStartInfo(app2PackageName, app2UidUser2, app2PidUser2, 0, list); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + app2ProcessName, // processName + ApplicationStartInfo.START_REASON_CONTENT_PROVIDER, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_WARM, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Case 8: Save and load again + ArrayList<ApplicationStartInfo> original = new ArrayList<ApplicationStartInfo>(); + mAppStartInfoTracker.getStartInfo(null, app1Uid, 0, 0, original); + assertTrue(original.size() > 0); + + mAppStartInfoTracker.persistProcessStartInfo(); + assertTrue(mAppStartInfoTracker.mProcStartInfoFile.exists()); + + mAppStartInfoTracker.clearProcessStartInfo(false); + list.clear(); + mAppStartInfoTracker.getStartInfo(null, app1Uid, 0, 0, list); + assertEquals(0, list.size()); + + mAppStartInfoTracker.loadExistingProcessStartInfo(); + list.clear(); + mAppStartInfoTracker.getStartInfo(null, app1Uid, 0, 0, list); + assertEquals(original.size(), list.size()); + + for (int i = list.size() - 1; i >= 0; i--) { + assertTrue(list.get(i).equals(original.get(i))); + } + } + + private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field mfield = Field.class.getDeclaredField("accessFlags"); + mfield.setAccessible(true); + mfield.setInt(field, mfield.getInt(field) & ~(Modifier.FINAL | Modifier.PRIVATE)); + field.set(obj, val); + } catch (NoSuchFieldException | IllegalAccessException e) { + } + } + + private void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, + String processName, String packageName) { + return makeProcessRecord(pid, uid, packageUid, definingUid, processName, packageName, mAms); + } + + @SuppressWarnings("GuardedBy") + static ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, + String processName, String packageName, ActivityManagerService ams) { + ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ProcessRecord app = new ProcessRecord(ams, ai, processName, uid); + app.setPid(pid); + app.info.uid = packageUid; + if (definingUid != null) { + app.setHostingRecord(HostingRecord.byAppZygote(COMPONENT, "", definingUid, "")); + } + return app; + } + + private static Intent buildIntent(ComponentName componentName) throws Exception { + Intent intent = new Intent(); + intent.setComponent(componentName); + intent.setPackage(componentName.getPackageName()); + return intent; + } + + private void verifyInProgressRecordsSize(int expectedSize) { + synchronized (mAppStartInfoTracker.mLock) { + assertEquals(mAppStartInfoTracker.mInProgRecords.size(), expectedSize); + } + } + + private void verifyInProgApplicationStartInfo(int index, + Integer pid, Integer uid, Integer packageUid, + Integer definingUid, String processName, + Integer reason, Integer startupState, Integer startType, Integer launchMode) { + synchronized (mAppStartInfoTracker.mLock) { + verifyApplicationStartInfo(mAppStartInfoTracker.mInProgRecords.valueAt(index), + pid, uid, packageUid, definingUid, processName, reason, startupState, + startType, launchMode); + } + } + + private void verifyApplicationStartInfo(ApplicationStartInfo info, + Integer pid, Integer uid, Integer packageUid, + Integer definingUid, String processName, + Integer reason, Integer startupState, Integer startType, Integer launchMode) { + assertNotNull(info); + + if (pid != null) { + assertEquals(pid.intValue(), info.getPid()); + } + if (uid != null) { + assertEquals(uid.intValue(), info.getRealUid()); + } + if (packageUid != null) { + assertEquals(packageUid.intValue(), info.getPackageUid()); + } + if (definingUid != null) { + assertEquals(definingUid.intValue(), info.getDefiningUid()); + } + if (processName != null) { + assertTrue(TextUtils.equals(processName, info.getProcessName())); + } + if (reason != null) { + assertEquals(reason.intValue(), info.getReason()); + } + if (startupState != null) { + assertEquals(startupState.intValue(), info.getStartupState()); + } + if (startType != null) { + assertEquals(startType.intValue(), info.getStartType()); + } + if (launchMode != null) { + assertEquals(launchMode.intValue(), info.getLaunchMode()); + } + } + + private class TestInjector extends Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile, + Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandler; + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mProcessList; + } + } + + static class ServiceThreadRule implements TestRule { + + private ServiceThread mThread; + + ServiceThread getThread() { + return mThread; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + mThread = new ServiceThread("TestServiceThread", + Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */); + mThread.start(); + try { + base.evaluate(); + } finally { + mThread.getThreadHandler().runWithScissors(mThread::quit, 0 /* timeout */); + } + } + }; + } + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("mockingservicestestjni"); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index 116d5db45023..284e491f8081 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -426,6 +426,8 @@ public class FlexibilityControllerTest { @Test public void testGetNextConstraintDropTimeElapsedLocked() { + setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS); + long nextTimeToDropNumConstraints; // no delay, deadline @@ -457,15 +459,18 @@ public class FlexibilityControllerTest { nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(130400100, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) / 2, + nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(156320100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 6 / 10, + nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(182240100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 7 / 10, + nextTimeToDropNumConstraints); // no delay, no deadline jb = createJob(0); @@ -473,15 +478,15 @@ public class FlexibilityControllerTest { nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(129600100, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(155520100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(181440100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints); // delay, deadline jb = createJob(0) @@ -950,8 +955,7 @@ public class FlexibilityControllerTest { mJobStore.add(js); // Needed because if before and after Uid bias is the same, nothing happens. - when(mJobSchedulerService.getUidBias(mSourceUid)) - .thenReturn(JobInfo.BIAS_DEFAULT); + doReturn(JobInfo.BIAS_DEFAULT).when(mJobSchedulerService).getUidBias(mSourceUid); synchronized (mFlexibilityController.mLock) { mFlexibilityController.maybeStartTrackingJobLocked(js, null); @@ -1328,7 +1332,7 @@ public class FlexibilityControllerTest { final ArraySet<String> pkgs = new ArraySet<>(); pkgs.add(js.getSourcePackageName()); - when(mJobSchedulerService.getPackagesForUidLocked(mSourceUid)).thenReturn(pkgs); + doReturn(pkgs).when(mJobSchedulerService).getPackagesForUidLocked(mSourceUid); setUidBias(mSourceUid, BIAS_TOP_APP); setUidBias(mSourceUid, BIAS_FOREGROUND_SERVICE); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index 0403c64fc624..ec7e35982311 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -233,7 +233,7 @@ public class PackageArchiverTest { Exception e = assertThrows( SecurityException.class, () -> mArchiveManager.requestArchive(PACKAGE, "different", mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e).hasMessageThat().isEqualTo( String.format( "The UID %s of callerPackageName set by the caller doesn't match the " @@ -250,7 +250,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( String.format("Package %s not found.", PACKAGE)); @@ -260,8 +260,7 @@ public class PackageArchiverTest { public void archiveApp_packageNotInstalledForUser() throws IntentSender.SendIntentException { mPackageSetting.modifyUserState(UserHandle.CURRENT.getIdentifier()).setInstalled(false); - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - 0); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -291,7 +290,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo("No installer found"); } @@ -305,7 +304,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( "Installer does not support unarchival"); @@ -319,7 +318,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( TextUtils.formatSimple("The app %s does not have a main activity.", PACKAGE)); @@ -331,8 +330,7 @@ public class PackageArchiverTest { doThrow(e).when(mArchiveManager).storeIcon(eq(PACKAGE), any(LauncherActivityInfo.class), eq(mUserId), anyInt(), anyInt()); - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - 0); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -355,7 +353,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( TextUtils.formatSimple("The app %s is opted out of archiving.", PACKAGE)); @@ -363,8 +361,7 @@ public class PackageArchiverTest { @Test public void archiveApp_withNoAdditionalFlags_success() { - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - 0); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); verify(mInstallerService).uninstall( @@ -386,14 +383,13 @@ public class PackageArchiverTest { @Test public void archiveApp_withAdditionalFlags_success() { - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - PackageManager.DELETE_SHOW_DIALOG); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); verify(mInstallerService).uninstall( eq(new VersionedPackage(PACKAGE, PackageManager.VERSION_CODE_HIGHEST)), eq(CALLER_PACKAGE), - eq(DELETE_ARCHIVE | DELETE_KEEP_DATA | PackageManager.DELETE_SHOW_DIALOG), + eq(DELETE_ARCHIVE | DELETE_KEEP_DATA), eq(mIntentSender), eq(UserHandle.CURRENT.getIdentifier()), anyInt()); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index fd6aa0c1ffab..e6298eeccafb 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -19,9 +19,11 @@ import static android.os.UserManager.DISALLOW_OUTGOING_CALLS; import static android.os.UserManager.DISALLOW_SMS; import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; +import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -30,20 +32,27 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; +import android.app.KeyguardManager; import android.content.Context; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; +import android.multiuser.Flags; +import android.os.PowerManager; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.util.Log; import android.util.Pair; @@ -52,6 +61,7 @@ import android.util.Xml; import androidx.test.annotation.UiThreadTest; +import com.android.dx.mockito.inline.extended.MockedVoidMethod; import com.android.internal.widget.LockSettingsInternal; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.testing.ExtendedMockitoRule; @@ -65,6 +75,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; +import org.mockito.Mockito; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -115,8 +126,11 @@ public final class UserManagerServiceTest { .spyStatic(LocalServices.class) .spyStatic(SystemProperties.class) .mockStatic(Settings.Global.class) + .mockStatic(Settings.Secure.class) .build(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final Object mPackagesLock = new Object(); private final Context mRealContext = androidx.test.InstrumentationRegistry.getInstrumentation() .getTargetContext(); @@ -133,6 +147,8 @@ public final class UserManagerServiceTest { private @Mock StorageManager mStorageManager; private @Mock LockSettingsInternal mLockSettingsInternal; private @Mock PackageManagerInternal mPackageManagerInternal; + private @Mock KeyguardManager mKeyguardManager; + private @Mock PowerManager mPowerManager; /** * Reference to the {@link UserManagerService} being tested. @@ -156,6 +172,8 @@ public final class UserManagerServiceTest { when(mDeviceStorageMonitorInternal.isMemoryLow()).thenReturn(false); mockGetLocalService(DeviceStorageMonitorInternal.class, mDeviceStorageMonitorInternal); when(mSpiedContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager); + when(mSpiedContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager); + when(mSpiedContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager); mockGetLocalService(LockSettingsInternal.class, mLockSettingsInternal); mockGetLocalService(PackageManagerInternal.class, mPackageManagerInternal); doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any()); @@ -550,6 +568,143 @@ public final class UserManagerServiceTest { assertTrue(hasRestrictionsInUserXMLFile(user.id)); } + @Test + public void testAutoLockOnDeviceLockForPrivateProfile() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK); + Mockito.doNothing().when(mSpiedUms).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), any(), + any()); + + mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(true); + + Mockito.verify(mSpiedUms).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test + public void testAutoLockOnDeviceLockForPrivateProfile_keyguardUnlocked() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK); + + mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(false); + + // Verify that no operation to disable quiet mode is not called + Mockito.verify(mSpiedUms, never()).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test + public void testAutoLockOnDeviceLockForPrivateProfile_flagDisabled() { + mSetFlagsRule.disableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + + mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(true); + + // Verify that no auto-lock operations take place + verify((MockedVoidMethod) () -> Settings.Secure.getInt(any(), + eq(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), anyInt()), never()); + Mockito.verify(mSpiedUms, never()).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test + public void testAutoLockAfterInactityForPrivateProfile() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); + when(mPowerManager.isInteractive()).thenReturn(false); + + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + Mockito.doNothing().when(mSpiedUms).scheduleMessageToAutoLockPrivateSpace( + eq(privateProfileUser.getUserHandle().getIdentifier()), any(), + anyLong()); + + + mSpiedUms.maybeScheduleMessageToAutoLockPrivateSpace(); + + Mockito.verify(mSpiedUms).scheduleMessageToAutoLockPrivateSpace( + eq(privateProfileUser.getUserHandle().getIdentifier()), any(), anyLong()); + } + + @Test + public void testSetOrUpdateAutoLockPreference_noPrivateProfile() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); + + Mockito.verify(mSpiedContext, never()).registerReceiver(any(), any(), any(), any()); + Mockito.verify(mSpiedContext, never()).unregisterReceiver(any()); + Mockito.verify(mKeyguardManager, never()).removeKeyguardLockedStateListener((any())); + Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); + } + + @Test + public void testSetOrUpdateAutoLockPreference() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + mUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + + // Set the preference to auto lock on device lock + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK); + + // Verify that keyguard state listener was added + Mockito.verify(mKeyguardManager).addKeyguardLockedStateListener(any(), any()); + //Verity that keyguard state listener was not removed + Mockito.verify(mKeyguardManager, never()).removeKeyguardLockedStateListener(any()); + // Broadcasts are already unregistered when UserManagerService starts and the flag + // isDeviceInactivityBroadcastReceiverRegistered is false + Mockito.verify(mSpiedContext, never()).registerReceiver(any(), any(), any(), any()); + Mockito.verify(mSpiedContext, never()).unregisterReceiver(any()); + + Mockito.clearInvocations(mKeyguardManager); + Mockito.clearInvocations(mSpiedContext); + + // Now set the preference to auto-lock on inactivity + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); + + // Verify that inactivity broadcasts are registered + Mockito.verify(mSpiedContext, times(2)).registerReceiver(any(), any(), any(), any()); + // Verify that keyguard state listener is removed + Mockito.verify(mKeyguardManager).removeKeyguardLockedStateListener(any()); + // Verify that all other operations don't take place + Mockito.verify(mSpiedContext, never()).unregisterReceiver(any()); + Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); + + Mockito.clearInvocations(mKeyguardManager); + Mockito.clearInvocations(mSpiedContext); + + // Finally, set the preference to don't auto-lock + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER); + + // Verify that inactivity broadcasts are unregistered and keyguard listener was removed + Mockito.verify(mSpiedContext).unregisterReceiver(any()); + Mockito.verify(mKeyguardManager).removeKeyguardLockedStateListener(any()); + // Verify that no broadcasts were registered and no listeners were added + Mockito.verify(mSpiedContext, never()).registerReceiver(any(), any(), any(), any()); + Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); + } + /** * Returns true if the user's XML file has Default restrictions * @param userId Id of the user. @@ -632,6 +787,12 @@ public final class UserManagerServiceTest { SystemProperties.getBoolean(eq("fw.show_multiuserui"), anyBoolean())); } + private void mockAutoLockForPrivateSpace(int val) { + doReturn(val).when(() -> + Settings.Secure.getIntForUser(any(), eq(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), + anyInt(), anyInt())); + } + private void mockCurrentUser(@UserIdInt int userId) { mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal); diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java index 37ca09d9fa27..b41568298dbc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -47,6 +47,10 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.SensorProperties; +import android.hardware.face.FaceManager; +import android.hardware.face.FaceSensorProperties; +import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -69,6 +73,7 @@ import android.view.WindowManagerGlobal; import androidx.test.core.app.ApplicationProvider; import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -105,6 +110,8 @@ public class TrustManagerServiceTest { private static final String URI_SCHEME_PACKAGE = "package"; private static final int TEST_USER_ID = 50; + private static final UserInfo TEST_USER = + new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL); private static final int PARENT_USER_ID = 60; private static final int PROFILE_USER_ID = 70; private static final long[] PARENT_BIOMETRIC_SIDS = new long[] { 600L, 601L }; @@ -117,6 +124,8 @@ public class TrustManagerServiceTest { private @Mock ActivityManager mActivityManager; private @Mock BiometricManager mBiometricManager; private @Mock DevicePolicyManager mDevicePolicyManager; + private @Mock FaceManager mFaceManager; + private @Mock FingerprintManager mFingerprintManager; private @Mock IKeystoreAuthorization mKeystoreAuthorization; private @Mock LockPatternUtils mLockPatternUtils; private @Mock PackageManager mPackageManager; @@ -133,6 +142,9 @@ public class TrustManagerServiceTest { when(mActivityManager.isUserRunning(TEST_USER_ID)).thenReturn(true); doReturn(mock(IActivityManager.class)).when(() -> ActivityManager.getService()); + when(mFaceManager.getSensorProperties()).thenReturn(List.of()); + when(mFingerprintManager.getSensorProperties()).thenReturn(List.of()); + doReturn(mKeystoreAuthorization).when(() -> Authorization.getService()); when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager); @@ -161,13 +173,16 @@ public class TrustManagerServiceTest { when(mPackageManager.checkPermission(any(), any())).thenReturn( PackageManager.PERMISSION_GRANTED); - when(mUserManager.getAliveUsers()).thenReturn( - List.of(new UserInfo(TEST_USER_ID, "user", UserInfo.FLAG_FULL))); + when(mUserManager.getAliveUsers()).thenReturn(List.of(TEST_USER)); + when(mUserManager.getEnabledProfileIds(TEST_USER_ID)).thenReturn(new int[0]); + when(mUserManager.getUserInfo(TEST_USER_ID)).thenReturn(TEST_USER); when(mWindowManager.isKeyguardLocked()).thenReturn(true); mMockContext.addMockSystemService(ActivityManager.class, mActivityManager); mMockContext.addMockSystemService(BiometricManager.class, mBiometricManager); + mMockContext.addMockSystemService(FaceManager.class, mFaceManager); + mMockContext.addMockSystemService(FingerprintManager.class, mFingerprintManager); mMockContext.setMockPackageManager(mPackageManager); mMockContext.addMockSystemService(UserManager.class, mUserManager); doReturn(mWindowManager).when(() -> WindowManagerGlobal.getWindowManagerService()); @@ -362,9 +377,9 @@ public class TrustManagerServiceTest { when(mWindowManager.isKeyguardLocked()).thenReturn(true); mTrustManager.reportKeyguardShowingChanged(); verify(mKeystoreAuthorization) - .onDeviceLocked(eq(PARENT_USER_ID), eq(PARENT_BIOMETRIC_SIDS)); + .onDeviceLocked(eq(PARENT_USER_ID), eq(PARENT_BIOMETRIC_SIDS), eq(false)); verify(mKeystoreAuthorization) - .onDeviceLocked(eq(PROFILE_USER_ID), eq(PARENT_BIOMETRIC_SIDS)); + .onDeviceLocked(eq(PROFILE_USER_ID), eq(PARENT_BIOMETRIC_SIDS), eq(false)); } // Tests that when the device is locked for a managed profile with a *separate* challenge, the @@ -381,7 +396,188 @@ public class TrustManagerServiceTest { mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, true); verify(mKeystoreAuthorization) - .onDeviceLocked(eq(PROFILE_USER_ID), eq(PROFILE_BIOMETRIC_SIDS)); + .onDeviceLocked(eq(PROFILE_USER_ID), eq(PROFILE_BIOMETRIC_SIDS), eq(false)); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockEnabled_whenWeakFingerprintIsSetupAndAllowed() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFingerprint(SensorProperties.STRENGTH_WEAK); + verifyWeakUnlockEnabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockEnabled_whenWeakFaceIsSetupAndAllowed() throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFace(SensorProperties.STRENGTH_WEAK); + verifyWeakUnlockEnabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockEnabled_whenConvenienceFingerprintIsSetupAndAllowed() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFingerprint(SensorProperties.STRENGTH_CONVENIENCE); + verifyWeakUnlockEnabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockEnabled_whenConvenienceFaceIsSetupAndAllowed() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFace(SensorProperties.STRENGTH_CONVENIENCE); + verifyWeakUnlockEnabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenStrongAuthRequired() throws Exception { + setupStrongAuthTracker(StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, true); + setupFace(SensorProperties.STRENGTH_WEAK); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenNonStrongBiometricNotAllowed() throws Exception { + setupStrongAuthTracker(StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED, + /* isNonStrongBiometricAllowed= */ false); + setupFace(SensorProperties.STRENGTH_WEAK); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenWeakFingerprintSensorIsPresentButNotEnrolled() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFingerprint(SensorProperties.STRENGTH_WEAK, /* enrolled= */ false); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenWeakFaceSensorIsPresentButNotEnrolled() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFace(SensorProperties.STRENGTH_WEAK, /* enrolled= */ false); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void + testKeystoreWeakUnlockDisabled_whenWeakFingerprintIsSetupButForbiddenByDevicePolicy() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFingerprint(SensorProperties.STRENGTH_WEAK); + when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, TEST_USER_ID)) + .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenWeakFaceIsSetupButForbiddenByDevicePolicy() + throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFace(SensorProperties.STRENGTH_WEAK); + when(mDevicePolicyManager.getKeyguardDisabledFeatures(null, TEST_USER_ID)) + .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FACE); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenOnlyStrongFingerprintIsSetup() throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFingerprint(SensorProperties.STRENGTH_STRONG); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenOnlyStrongFaceIsSetup() throws Exception { + setupStrongAuthTrackerToAllowEverything(); + setupFace(SensorProperties.STRENGTH_STRONG); + verifyWeakUnlockDisabled(); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testKeystoreWeakUnlockDisabled_whenNoBiometricsAreSetup() throws Exception { + setupStrongAuthTrackerToAllowEverything(); + verifyWeakUnlockDisabled(); + } + + private void setupStrongAuthTrackerToAllowEverything() throws Exception { + setupStrongAuthTracker(StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED, true); + } + + private void setupStrongAuthTracker(int strongAuthFlags, boolean isNonStrongBiometricAllowed) + throws Exception { + bootService(); + mService.onUserSwitching(null, new SystemService.TargetUser(TEST_USER)); + + ArgumentCaptor<StrongAuthTracker> strongAuthTracker = + ArgumentCaptor.forClass(StrongAuthTracker.class); + verify(mLockPatternUtils).registerStrongAuthTracker(strongAuthTracker.capture()); + strongAuthTracker.getValue().getStub().onStrongAuthRequiredChanged( + strongAuthFlags, TEST_USER_ID); + strongAuthTracker.getValue().getStub().onIsNonStrongBiometricAllowedChanged( + isNonStrongBiometricAllowed, TEST_USER_ID); + mService.waitForIdle(); + } + + private void setupFingerprint(int strength) { + setupFingerprint(strength, /* enrolled= */ true); + } + + private void setupFingerprint(int strength, boolean enrolled) { + int sensorId = 100; + List<SensorProperties.ComponentInfo> componentInfo = List.of(); + SensorProperties sensor = new SensorProperties(sensorId, strength, componentInfo); + when(mFingerprintManager.getSensorProperties()).thenReturn(List.of(sensor)); + when(mFingerprintManager.hasEnrolledTemplates(TEST_USER_ID)).thenReturn(enrolled); + } + + private void setupFace(int strength) { + setupFace(strength, /* enrolled= */ true); + } + + private void setupFace(int strength, boolean enrolled) { + int sensorId = 100; + List<SensorProperties.ComponentInfo> componentInfo = List.of(); + FaceSensorProperties sensor = new FaceSensorProperties( + sensorId, strength, componentInfo, FaceSensorProperties.TYPE_RGB); + when(mFaceManager.getSensorProperties()).thenReturn(List.of(sensor)); + when(mFaceManager.hasEnrolledTemplates(TEST_USER_ID)).thenReturn(enrolled); + } + + private void verifyWeakUnlockEnabled() throws Exception { + verifyWeakUnlockValue(true); + } + + private void verifyWeakUnlockDisabled() throws Exception { + verifyWeakUnlockValue(false); + } + + // Simulates a device unlock and a device lock, then verifies that the expected + // weakUnlockEnabled flag was passed to Keystore's onDeviceLocked method. + private void verifyWeakUnlockValue(boolean expectedWeakUnlockEnabled) throws Exception { + when(mWindowManager.isKeyguardLocked()).thenReturn(false); + mTrustManager.reportKeyguardShowingChanged(); + verify(mKeystoreAuthorization).onDeviceUnlocked(TEST_USER_ID, null); + + when(mWindowManager.isKeyguardLocked()).thenReturn(true); + mTrustManager.reportKeyguardShowingChanged(); + verify(mKeystoreAuthorization).onDeviceLocked(eq(TEST_USER_ID), any(), + eq(expectedWeakUnlockEnabled)); } private void setupMocksForProfile(boolean unifiedChallenge) { diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 00450267ee79..0831086b28ca 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -29,8 +29,6 @@ android_test { "src/**/*.java", "src/**/*.kt", - "test-apps/JobTestApp/src/**/*.java", - "test-apps/SuspendTestApp/src/**/*.java", ], static_libs: [ @@ -124,7 +122,6 @@ android_test { }, data: [ - ":JobTestApp", ":SimpleServiceTestApp1", ":SimpleServiceTestApp2", ":SimpleServiceTestApp3", diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml index b1d50399416a..27c522d68119 100644 --- a/services/tests/servicestests/AndroidTest.xml +++ b/services/tests/servicestests/AndroidTest.xml @@ -29,7 +29,6 @@ <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> <option name="test-file-name" value="FrameworksServicesTests.apk" /> - <option name="test-file-name" value="JobTestApp.apk" /> <option name="test-file-name" value="SuspendTestApp.apk" /> <option name="test-file-name" value="SimpleServiceTestApp1.apk" /> <option name="test-file-name" value="SimpleServiceTestApp2.apk" /> diff --git a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java b/services/tests/servicestests/src/com/android/server/BootReceiverTest.java deleted file mode 100644 index 523c5c060cf5..000000000000 --- a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server; - -import static com.google.common.truth.Truth.assertThat; - -import android.test.AndroidTestCase; - -import com.android.server.os.TombstoneProtos; -import com.android.server.os.TombstoneProtos.Tombstone; - -public class BootReceiverTest extends AndroidTestCase { - private static final String TAG = "BootReceiverTest"; - - public void testRemoveMemoryFromTombstone() { - Tombstone tombstoneBase = Tombstone.newBuilder() - .setBuildFingerprint("build_fingerprint") - .setRevision("revision") - .setPid(123) - .setTid(23) - .setUid(34) - .setSelinuxLabel("selinux_label") - .addCommandLine("cmd1") - .addCommandLine("cmd2") - .addCommandLine("cmd3") - .setProcessUptime(300) - .setAbortMessage("abort") - .addCauses(TombstoneProtos.Cause.newBuilder() - .setHumanReadable("cause1") - .setMemoryError(TombstoneProtos.MemoryError.newBuilder() - .setTool(TombstoneProtos.MemoryError.Tool.SCUDO) - .setType(TombstoneProtos.MemoryError.Type.DOUBLE_FREE))) - .addLogBuffers(TombstoneProtos.LogBuffer.newBuilder().setName("name").addLogs( - TombstoneProtos.LogMessage.newBuilder() - .setTimestamp("123") - .setMessage("message"))) - .addOpenFds(TombstoneProtos.FD.newBuilder().setFd(1).setPath("path")) - .build(); - - Tombstone tombstoneWithoutMemory = tombstoneBase.toBuilder() - .putThreads(1, TombstoneProtos.Thread.newBuilder() - .setId(1) - .setName("thread1") - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r1").setU64(1)) - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r2").setU64(2)) - .addBacktraceNote("backtracenote1") - .addUnreadableElfFiles("files1") - .setTaggedAddrCtrl(1) - .setPacEnabledKeys(10) - .build()) - .build(); - - Tombstone tombstoneWithMemory = tombstoneBase.toBuilder() - .addMemoryMappings(TombstoneProtos.MemoryMapping.newBuilder() - .setBeginAddress(1) - .setEndAddress(100) - .setOffset(10) - .setRead(true) - .setWrite(true) - .setExecute(false) - .setMappingName("mapping") - .setBuildId("build") - .setLoadBias(70)) - .putThreads(1, TombstoneProtos.Thread.newBuilder() - .setId(1) - .setName("thread1") - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r1").setU64(1)) - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r2").setU64(2)) - .addBacktraceNote("backtracenote1") - .addUnreadableElfFiles("files1") - .addMemoryDump(TombstoneProtos.MemoryDump.newBuilder() - .setRegisterName("register1") - .setMappingName("mapping") - .setBeginAddress(10)) - .setTaggedAddrCtrl(1) - .setPacEnabledKeys(10) - .build()) - .build(); - - assertThat(BootReceiver.removeMemoryFromTombstone(tombstoneWithMemory)) - .isEqualTo(tombstoneWithoutMemory); - } -} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index 543fa5727f19..62073497b735 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -1775,6 +1775,11 @@ public class HdmiCecLocalDevicePlaybackTest { @Test public void wakeUp_hotPlugIn_invokesDeviceDiscoveryOnce() { + // There might be a leftover HotplugDetectionAction that can interfere with the test. + mHdmiCecLocalDevicePlayback.removeAction(HotplugDetectionAction.class); + + long pollingDelay = TimeUnit.SECONDS.toMillis(60); + mHdmiCecController.setPollDevicesDelay(pollingDelay); mNativeWrapper.setPollAddressResponse(Constants.ADDR_PLAYBACK_2, SendMessageResult.SUCCESS); mHdmiControlService.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON); mTestLooper.dispatchAll(); @@ -1783,6 +1788,14 @@ public class HdmiCecLocalDevicePlaybackTest { mTestLooper.dispatchAll(); assertThat(mHdmiCecLocalDevicePlayback.getActions(DeviceDiscoveryAction.class)).hasSize(1); + mTestLooper.moveTimeForward(pollingDelay); + mTestLooper.dispatchAll(); + + HdmiCecMessage givePhysicalAddress = HdmiCecMessageBuilder.buildGivePhysicalAddress( + Constants.ADDR_PLAYBACK_1, + Constants.ADDR_PLAYBACK_2); + assertThat(mNativeWrapper.getResultMessages().stream() + .filter(message -> message.equals(givePhysicalAddress)).count()).isEqualTo(1); } @Test diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java deleted file mode 100644 index e871fc567107..000000000000 --- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java +++ /dev/null @@ -1,233 +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.job; - -import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STARTED; -import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STOPPED; -import static com.android.servicestests.apps.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.app.ActivityManager; -import android.app.AppOpsManager; -import android.app.IActivityManager; -import android.app.job.JobParameters; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.IDeviceIdleController; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.Log; - -import androidx.test.InstrumentationRegistry; -import androidx.test.filters.FlakyTest; -import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.servicestests.apps.jobtestapp.TestJobActivity; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests that background restrictions on jobs work as expected. - * This test requires test-apps/JobTestApp to be installed on the device. - * To run this test from root of checkout: - * <pre> - * mmm -j32 frameworks/base/services/tests/servicestests/ - * adb install -r $OUT/data/app/JobTestApp/JobTestApp.apk - * adb install -r $OUT/data/app/FrameworksServicesTests/FrameworksServicesTests.apk - * adb shell am instrument -e class 'com.android.server.job.BackgroundRestrictionsTest' -w \ - * com.android.frameworks.servicestests - * </pre> - */ -@RunWith(AndroidJUnit4.class) -@LargeTest -public class BackgroundRestrictionsTest { - private static final String TAG = BackgroundRestrictionsTest.class.getSimpleName(); - private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp"; - private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity"; - private static final long POLL_INTERVAL = 500; - private static final long DEFAULT_WAIT_TIMEOUT = 10_000; - - private Context mContext; - private AppOpsManager mAppOpsManager; - private IDeviceIdleController mDeviceIdleController; - private IActivityManager mIActivityManager; - private volatile int mTestJobId = -1; - private int mTestPackageUid; - /* accesses must be synchronized on itself */ - private final TestJobStatus mTestJobStatus = new TestJobStatus(); - private final BroadcastReceiver mJobStateChangeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY); - Log.d(TAG, "Received action " + intent.getAction()); - synchronized (mTestJobStatus) { - switch (intent.getAction()) { - case ACTION_JOB_STARTED: - mTestJobStatus.running = true; - mTestJobStatus.jobId = params.getJobId(); - mTestJobStatus.stopReason = JobParameters.STOP_REASON_UNDEFINED; - break; - case ACTION_JOB_STOPPED: - mTestJobStatus.running = false; - mTestJobStatus.jobId = params.getJobId(); - mTestJobStatus.stopReason = params.getStopReason(); - break; - } - } - } - }; - - @Before - public void setUp() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); - mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); - mDeviceIdleController = IDeviceIdleController.Stub.asInterface( - ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); - mIActivityManager = ActivityManager.getService(); - mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0); - mTestJobStatus.reset(); - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ACTION_JOB_STARTED); - intentFilter.addAction(ACTION_JOB_STOPPED); - mContext.registerReceiver(mJobStateChangeReceiver, intentFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); - setAppOpsModeAllowed(true); - setPowerExemption(false); - } - - private void scheduleTestJob() { - mTestJobId = (int) (SystemClock.uptimeMillis() / 1000); - final Intent scheduleJobIntent = new Intent(TestJobActivity.ACTION_START_JOB); - scheduleJobIntent.putExtra(TestJobActivity.EXTRA_JOB_ID_KEY, mTestJobId); - scheduleJobIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY)); - mContext.startActivity(scheduleJobIntent); - } - - private void scheduleAndAssertJobStarted() throws Exception { - scheduleTestJob(); - Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY); - assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - } - - @FlakyTest - @Test - public void testPowerExemption() throws Exception { - scheduleAndAssertJobStarted(); - setAppOpsModeAllowed(false); - mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT); - assertTrue("Job did not stop after putting app under bg-restriction", - awaitJobStop(DEFAULT_WAIT_TIMEOUT, - JobParameters.STOP_REASON_BACKGROUND_RESTRICTION)); - - setPowerExemption(true); - scheduleTestJob(); - Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY); - assertTrue("Job did not start when the app was in the power exemption list", - awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - - setPowerExemption(false); - assertTrue("Job did not stop after removing from the power exemption list", - awaitJobStop(DEFAULT_WAIT_TIMEOUT, - JobParameters.STOP_REASON_BACKGROUND_RESTRICTION)); - - scheduleTestJob(); - Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY); - assertFalse("Job started under bg-restrictions", awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - setPowerExemption(true); - assertTrue("Job did not start when the app was in the power exemption list", - awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - } - - @After - public void tearDown() throws Exception { - final Intent cancelJobsIntent = new Intent(TestJobActivity.ACTION_CANCEL_JOBS); - cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY)); - cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(cancelJobsIntent); - mContext.unregisterReceiver(mJobStateChangeReceiver); - Thread.sleep(500); // To avoid race with register in the next setUp - setAppOpsModeAllowed(true); - setPowerExemption(false); - } - - private void setPowerExemption(boolean exempt) throws RemoteException { - if (exempt) { - mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE); - } else { - mDeviceIdleController.removePowerSaveWhitelistApp(TEST_APP_PACKAGE); - } - } - - private void setAppOpsModeAllowed(boolean allow) { - mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mTestPackageUid, - TEST_APP_PACKAGE, allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); - } - - private boolean awaitJobStart(long timeout) throws InterruptedException { - return waitUntilTrue(timeout, () -> { - synchronized (mTestJobStatus) { - return (mTestJobStatus.jobId == mTestJobId) && mTestJobStatus.running; - } - }); - } - - private boolean awaitJobStop(long timeout, @JobParameters.StopReason int expectedStopReason) - throws InterruptedException { - return waitUntilTrue(timeout, () -> { - synchronized (mTestJobStatus) { - return (mTestJobStatus.jobId == mTestJobId) && !mTestJobStatus.running - && (expectedStopReason == JobParameters.STOP_REASON_UNDEFINED - || mTestJobStatus.stopReason == expectedStopReason); - } - }); - } - - private boolean waitUntilTrue(long timeout, Condition condition) throws InterruptedException { - final long deadLine = SystemClock.uptimeMillis() + timeout; - do { - Thread.sleep(POLL_INTERVAL); - } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine); - return condition.isTrue(); - } - - private static final class TestJobStatus { - int jobId; - int stopReason; - boolean running; - - private void reset() { - running = false; - stopReason = jobId = 0; - } - } - - private interface Condition { - boolean isTrue(); - } -} diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 0f5fb91a140f..d50affba1ea1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -406,8 +406,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { public void testPushDynamicShortcut() { // Change the max number of shortcuts. - mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5"); - + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); setCaller(CALLING_PACKAGE_1, USER_0); final ShortcutInfo s1 = makeShortcut("s1"); @@ -545,6 +545,57 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_0)); } + public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() + throws InterruptedException { + mService.updateConfigurationLocked( + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500"); + + // Verify calls to UsageStatsManagerInternal#reportShortcutUsage are throttled. + setCaller(CALLING_PACKAGE_1, USER_0); + { + final ShortcutInfo si = makeShortcut("s0"); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_1), eq("s0"), eq(USER_0)); + Mockito.reset(mMockUsageStatsManagerInternal); + for (int i = 2; i <= 10; i++) { + final ShortcutInfo si = makeShortcut("s" + i); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage( + any(), any(), anyInt()); + + // Verify pkg2 isn't blocked by pkg1, but consecutive calls from pkg2 are throttled as well. + setCaller(CALLING_PACKAGE_2, USER_0); + { + final ShortcutInfo si = makeShortcut("s1"); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_2), eq("s1"), eq(USER_0)); + Mockito.reset(mMockUsageStatsManagerInternal); + for (int i = 2; i <= 10; i++) { + final ShortcutInfo si = makeShortcut("s" + i); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage( + any(), any(), anyInt()); + + Mockito.reset(mMockUsageStatsManagerInternal); + // Let time passes which resets the throttle + Thread.sleep(505); + // Verify UsageStatsManagerInternal#reportShortcutUsed can be called again + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.pushDynamicShortcut(makeShortcut("s10")); + setCaller(CALLING_PACKAGE_2, USER_0); + mManager.pushDynamicShortcut(makeShortcut("s10")); + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_1), any(), eq(USER_0)); + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_2), any(), eq(USER_0)); + } + public void testUnlimitedCalls() { setCaller(CALLING_PACKAGE_1, USER_0); diff --git a/services/tests/servicestests/test-apps/JobTestApp/Android.bp b/services/tests/servicestests/test-apps/JobTestApp/Android.bp deleted file mode 100644 index 6458bcd4fc8f..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/Android.bp +++ /dev/null @@ -1,37 +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 { - // 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_helper_app { - name: "JobTestApp", - - sdk_version: "current", - - srcs: ["**/*.java"], - - dex_preopt: { - enabled: false, - }, - optimize: { - enabled: false, - }, -} diff --git a/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml deleted file mode 100644 index ac35805e8af6..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.servicestests.apps.jobtestapp"> - - <application> - <service android:name=".TestJobService" - android:permission="android.permission.BIND_JOB_SERVICE" /> - <activity android:name=".TestJobActivity" - android:exported="true" /> - </application> - -</manifest>
\ No newline at end of file diff --git a/services/tests/servicestests/test-apps/JobTestApp/OWNERS b/services/tests/servicestests/test-apps/JobTestApp/OWNERS deleted file mode 100644 index 6f207fb1a00e..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/OWNERS +++ /dev/null @@ -1 +0,0 @@ -include /apex/jobscheduler/OWNERS diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java deleted file mode 100644 index 99eb196e3298..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java +++ /dev/null @@ -1,67 +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.servicestests.apps.jobtestapp; - -import android.app.Activity; -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -public class TestJobActivity extends Activity { - private static final String TAG = TestJobActivity.class.getSimpleName(); - private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp"; - - public static final String EXTRA_JOB_ID_KEY = PACKAGE_NAME + ".extra.JOB_ID"; - public static final String ACTION_START_JOB = PACKAGE_NAME + ".action.START_JOB"; - public static final String ACTION_CANCEL_JOBS = PACKAGE_NAME + ".action.CANCEL_JOBS"; - public static final int JOB_INITIAL_BACKOFF = 10_000; - public static final int JOB_MINIMUM_LATENCY = 5_000; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ComponentName jobServiceComponent = new ComponentName(this, TestJobService.class); - JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); - final Intent intent = getIntent(); - switch (intent.getAction()) { - case ACTION_CANCEL_JOBS: - jobScheduler.cancelAll(); - Log.d(TAG, "Cancelled all jobs for " + getPackageName()); - break; - case ACTION_START_JOB: - final int jobId = intent.getIntExtra(EXTRA_JOB_ID_KEY, hashCode()); - JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobServiceComponent) - .setBackoffCriteria(JOB_INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_LINEAR) - .setMinimumLatency(JOB_MINIMUM_LATENCY) - .setOverrideDeadline(JOB_MINIMUM_LATENCY); - final int result = jobScheduler.schedule(jobBuilder.build()); - if (result != JobScheduler.RESULT_SUCCESS) { - Log.e(TAG, "Could not schedule job " + jobId); - } else { - Log.d(TAG, "Successfully scheduled job with id " + jobId); - } - break; - default: - Log.e(TAG, "Unknown action " + intent.getAction()); - } - finish(); - } -}
\ No newline at end of file diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java deleted file mode 100644 index b8585f26c185..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java +++ /dev/null @@ -1,53 +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.servicestests.apps.jobtestapp; - -import android.annotation.TargetApi; -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.Intent; -import android.util.Log; - -@TargetApi(24) -public class TestJobService extends JobService { - private static final String TAG = TestJobService.class.getSimpleName(); - private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp"; - public static final String ACTION_JOB_STARTED = PACKAGE_NAME + ".action.JOB_STARTED"; - public static final String ACTION_JOB_STOPPED = PACKAGE_NAME + ".action.JOB_STOPPED"; - public static final String JOB_PARAMS_EXTRA_KEY = PACKAGE_NAME + ".extra.JOB_PARAMETERS"; - - @Override - public boolean onStartJob(JobParameters params) { - Log.i(TAG, "Test job executing: " + params.getJobId()); - Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED); - reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - sendBroadcast(reportJobStartIntent); - return true; - } - - @Override - public boolean onStopJob(JobParameters params) { - Log.i(TAG, "Test job stopped executing: " + params.getJobId()); - Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED); - reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - sendBroadcast(reportJobStopIntent); - // Deadline constraint is dropped on reschedule, so it's more reliable to use a new job. - return false; - } -} diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 4e1c72af2727..2f29d10ec2f9 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -47,6 +47,7 @@ android_test { "flag-junit", "notification_flags_lib", "platform-test-rules", + "SettingsLib", ], libs: [ diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index db4653208f62..839cf7ce4926 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -17,6 +17,9 @@ package com.android.server; import static android.Manifest.permission.MODIFY_DAY_NIGHT_MODE; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; @@ -32,6 +35,7 @@ import static com.android.server.UiModeManagerService.SUPPORTED_NIGHT_MODE_CUSTO import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.fail; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; @@ -65,6 +69,7 @@ import static org.testng.Assert.assertThrows; import android.Manifest; import android.app.Activity; import android.app.AlarmManager; +import android.app.Flags; import android.app.IOnProjectionStateChangedListener; import android.app.IUiModeManager; import android.content.BroadcastReceiver; @@ -84,6 +89,8 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.test.FakePermissionEnforcer; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; import android.test.mock.MockContentResolver; @@ -98,6 +105,7 @@ import com.android.server.wm.WindowManagerInternal; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -109,6 +117,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.List; +import java.util.Map; import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @@ -159,6 +168,11 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { private TwilightListener mTwilightListener; private FakePermissionEnforcer mPermissionEnforcer; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT); + + @Before public void setUp() { // The AIDL stub will use PermissionEnforcer to check permission from the caller. @@ -1437,6 +1451,51 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } + private void testAttentionModeThemeOverlay(boolean modeNight) throws RemoteException { + //setup + if (modeNight) { + mService.setNightMode(MODE_NIGHT_YES); + assertTrue(mUiManagerService.getConfiguration().isNightModeActive()); + } else { + mService.setNightMode(MODE_NIGHT_NO); + assertFalse(mUiManagerService.getConfiguration().isNightModeActive()); + } + + // attention modes with expected night modes + Map<Integer, Boolean> modes = Map.of( + MODE_ATTENTION_THEME_OVERLAY_OFF, modeNight, + MODE_ATTENTION_THEME_OVERLAY_DAY, false, + MODE_ATTENTION_THEME_OVERLAY_NIGHT, true + ); + + // test + for (int aMode : modes.keySet()) { + try { + mService.setAttentionModeThemeOverlay(aMode); + + int appliedAMode = mService.getAttentionModeThemeOverlay(); + boolean nMode = modes.get(aMode); + + assertEquals(aMode, appliedAMode); + assertEquals(isNightModeActivated(), nMode); + } catch (RemoteException e) { + fail("Error communicating with server: " + e.getMessage()); + } + } + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testAttentionModeThemeOverlay_nightModeDisabled() throws RemoteException { + testAttentionModeThemeOverlay(false); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testAttentionModeThemeOverlay_nightModeEnabled() throws RemoteException { + testAttentionModeThemeOverlay(true); + } + private void triggerDockIntent() { final Intent dockedIntent = new Intent(Intent.ACTION_DOCK_EVENT) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java index 30843d222742..3797dbb97213 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -16,7 +16,8 @@ package com.android.server.notification; -import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; @@ -121,28 +122,49 @@ public class DefaultDeviceEffectsApplierTest { verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); verify(mColorDisplayManager).setSaturationLevel(eq(0)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); } @Test - public void apply_removesPreviouslyAppliedEffects() { + public void apply_removesEffects() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) + .setShouldUseNightMode(true) .build(); mApplier.apply(previousEffects, UPDATE_ORIGIN_USER); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); + verify(mColorDisplayManager).setSaturationLevel(eq(0)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); mApplier.apply(noEffects, UPDATE_ORIGIN_USER); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); + verify(mColorDisplayManager).setSaturationLevel(eq(100)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.0f)); - verifyZeroInteractions(mColorDisplayManager, mUiModeManager); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_OFF)); + } + + @Test + public void apply_removesOnlyPreviouslyAppliedEffects() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() + .setShouldSuppressAmbientDisplay(true) + .build(); + mApplier.apply(previousEffects, UPDATE_ORIGIN_USER); + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); + + ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); + mApplier.apply(noEffects, UPDATE_ORIGIN_USER); + + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); + verifyZeroInteractions(mColorDisplayManager, mWallpaperManager, mUiModeManager); } @Test @@ -150,6 +172,7 @@ public class DefaultDeviceEffectsApplierTest { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mContext.addMockSystemService(ColorDisplayManager.class, null); mContext.addMockSystemService(WallpaperManager.class, null); + mApplier = new DefaultDeviceEffectsApplier(mContext); ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) @@ -177,7 +200,7 @@ public class DefaultDeviceEffectsApplierTest { verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); verify(mPowerManager, never()).suppressAmbientDisplay(anyString(), anyBoolean()); - verify(mUiModeManager, never()).setNightModeActivatedForCustomMode(anyInt(), anyBoolean()); + verify(mUiModeManager, never()).setAttentionModeThemeOverlay(anyInt()); } @Test @@ -223,8 +246,7 @@ public class DefaultDeviceEffectsApplierTest { screenOffReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); // So the effect is applied, and we stopped listening for this event. - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); verify(mContext).unregisterReceiver(eq(screenOffReceiver)); } @@ -239,8 +261,7 @@ public class DefaultDeviceEffectsApplierTest { origin.value()); // Effect was applied, and no broadcast receiver was registered. - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); verify(mContext, never()).registerReceiver(any(), any(), anyInt()); } @@ -256,8 +277,7 @@ public class DefaultDeviceEffectsApplierTest { origin.value()); // Effect was applied, and no broadcast receiver was registered. - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); verify(mContext, never()).registerReceiver(any(), any(), anyInt()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index c1f35ccb69e0..723ac15fb50f 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -13792,8 +13792,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); mBinderService.setNotificationPolicy("package", policy, false); - verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy), - eq(ZenModeConfig.UPDATE_ORIGIN_APP)); + verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy)); } @Test @@ -13859,7 +13858,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); } else { verify(zenModeHelper).applyGlobalPolicyAsImplicitZenRule(anyString(), anyInt(), - eq(policy), anyInt()); + eq(policy)); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java index 3d8ec2ec9277..f604f1e77cf4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java @@ -52,7 +52,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .setShouldUseNightMode(false) .setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true) - .setUserModifiedFields(8) .build(); assertThat(deviceEffects.shouldDimWallpaper()).isTrue(); @@ -65,7 +64,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse(); assertThat(deviceEffects.shouldUseNightMode()).isFalse(); assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue(); - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(8); } @Test @@ -97,7 +95,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMinimizeRadioUsage(true) .setShouldUseNightMode(true) .setShouldSuppressAmbientDisplay(true) - .setUserModifiedFields(6) .build(); Parcel parcel = Parcel.obtain(); @@ -116,7 +113,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(copy.shouldUseNightMode()).isTrue(); assertThat(copy.shouldSuppressAmbientDisplay()).isTrue(); assertThat(copy.shouldDisplayGrayscale()).isFalse(); - assertThat(copy.getUserModifiedFields()).isEqualTo(6); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index db92719ecfd6..e523e79f6370 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -285,7 +285,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders()); assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders()); assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels()); - assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields()); } @Test @@ -342,45 +341,32 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = null; rule.zenDeviceEffects = null; - assertThat(rule.canBeUpdatedByApp()).isTrue(); rule.userModifiedFields = 1; + assertThat(rule.canBeUpdatedByApp()).isFalse(); } @Test public void testCanBeUpdatedByApp_policyModified() throws Exception { - ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0); - ZenPolicy policy = policyBuilder.build(); - ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); - rule.zenPolicy = policy; - - assertThat(rule.userModifiedFields).isEqualTo(0); + rule.zenPolicy = new ZenPolicy(); assertThat(rule.canBeUpdatedByApp()).isTrue(); - policy = policyBuilder.setUserModifiedFields(1).build(); - assertThat(policy.getUserModifiedFields()).isEqualTo(1); - rule.zenPolicy = policy; + rule.zenPolicyUserModifiedFields = 1; + assertThat(rule.canBeUpdatedByApp()).isFalse(); } @Test public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception { - ZenDeviceEffects.Builder deviceEffectsBuilder = - new ZenDeviceEffects.Builder().setUserModifiedFields(0); - ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); - ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); - rule.zenDeviceEffects = deviceEffects; - - assertThat(rule.userModifiedFields).isEqualTo(0); + rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build(); assertThat(rule.canBeUpdatedByApp()).isTrue(); - deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build(); - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1); - rule.zenDeviceEffects = deviceEffects; + rule.zenDeviceEffectsUserModifiedFields = 1; + assertThat(rule.canBeUpdatedByApp()).isFalse(); } @@ -406,6 +392,8 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; rule.userModifiedFields = 16; + rule.zenPolicyUserModifiedFields = 5; + rule.zenDeviceEffectsUserModifiedFields = 2; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); @@ -432,6 +420,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.iconResName, parceled.iconResName); assertEquals(rule.type, parceled.type); assertEquals(rule.userModifiedFields, parceled.userModifiedFields); + assertEquals(rule.zenPolicyUserModifiedFields, parceled.zenPolicyUserModifiedFields); + assertEquals(rule.zenDeviceEffectsUserModifiedFields, + parceled.zenDeviceEffectsUserModifiedFields); assertEquals(rule.triggerDescription, parceled.triggerDescription); assertEquals(rule.zenPolicy, parceled.zenPolicy); assertEquals(rule.deletionInstant, parceled.deletionInstant); @@ -511,6 +502,8 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; rule.userModifiedFields = 4; + rule.zenPolicyUserModifiedFields = 5; + rule.zenDeviceEffectsUserModifiedFields = 2; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); @@ -541,6 +534,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation); assertEquals(rule.type, fromXml.type); assertEquals(rule.userModifiedFields, fromXml.userModifiedFields); + assertEquals(rule.zenPolicyUserModifiedFields, fromXml.zenPolicyUserModifiedFields); + assertEquals(rule.zenDeviceEffectsUserModifiedFields, + fromXml.zenDeviceEffectsUserModifiedFields); assertEquals(rule.triggerDescription, fromXml.triggerDescription); assertEquals(rule.iconResName, fromXml.iconResName); assertEquals(rule.deletionInstant, fromXml.deletionInstant); @@ -697,7 +693,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { .allowPriorityChannels(false) .hideAllVisualEffects() .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true) - .setUserModifiedFields(4) .build(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -732,7 +727,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(policy.getVisualEffectAmbient(), fromXml.getVisualEffectAmbient()); assertEquals(policy.getVisualEffectNotificationList(), fromXml.getVisualEffectNotificationList()); - assertEquals(policy.getUserModifiedFields(), fromXml.getUserModifiedFields()); } private ZenModeConfig getMutedRingerConfig() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 9d7cf53e62db..2e64645ecade 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -73,13 +73,15 @@ public class ZenModeDiffTest extends UiServiceTestCase { : Set.of("version", "manualRule", "automaticRules"); // Differences for flagged fields are only generated if the flag is enabled. - // "Metadata" fields (userModifiedFields, deletionInstant) are not compared. + // "Metadata" fields (userModifiedFields & co, deletionInstant) are not compared. private static final Set<String> ZEN_RULE_EXEMPT_FIELDS = android.app.Flags.modesApi() - ? Set.of("userModifiedFields", "deletionInstant") + ? Set.of("userModifiedFields", "zenPolicyUserModifiedFields", + "zenDeviceEffectsUserModifiedFields", "deletionInstant") : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION, RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL, RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields", + "zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields", "deletionInstant"); // allowPriorityChannels is flagged by android.app.modes_api 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 78fe41f31849..edc876aab388 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -46,6 +46,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; import static android.service.notification.Condition.SOURCE_SCHEDULE; import static android.service.notification.Condition.SOURCE_USER_ACTION; @@ -67,6 +68,7 @@ import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; @@ -295,6 +297,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(appInfoSpy.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL); when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt())) .thenReturn(appInfoSpy); + when(mPackageManager.getApplicationInfo(eq(mContext.getPackageName()), anyInt())) + .thenReturn(appInfoSpy); mZenModeHelper.mPm = mPackageManager; mZenModeEventLogger.reset(); @@ -2236,12 +2240,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - // savedRule.getDeviceEffects() is equal to zde, except for the userModifiedFields. - // So we clear before comparing. - ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects()) - .setUserModifiedFields(0).build(); - - assertThat(savedEffects).isEqualTo(zde); + assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); } @Test @@ -2331,12 +2330,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - // savedRule.getDeviceEffects() is equal to updateFromUser, except for the - // userModifiedFields, so we clear before comparing. - ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects()) - .setUserModifiedFields(0).build(); - - assertThat(savedEffects).isEqualTo(updateFromUser); + assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser); } @Test @@ -3411,7 +3405,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; - rule.userModifiedFields = AutomaticZenRule.FIELD_NAME; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; @@ -3426,7 +3419,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(POLICY, actual.getZenPolicy()); assertEquals(CONFIG_ACTIVITY, actual.getConfigurationActivity()); assertEquals(TYPE, actual.getType()); - assertEquals(AutomaticZenRule.FIELD_NAME, actual.getUserModifiedFields()); assertEquals(ALLOW_MANUAL, actual.isManualInvocationAllowed()); assertEquals(CREATION_TIME, actual.getCreationTime()); assertEquals(OWNER.getPackageName(), actual.getPackageName()); @@ -3453,29 +3445,31 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setManualInvocationAllowed(ALLOW_MANUAL) .build(); - ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); - - mZenModeHelper.populateZenRule(OWNER.getPackageName(), azr, rule, UPDATE_ORIGIN_APP, true); - - assertEquals(NAME, rule.name); - assertEquals(OWNER, rule.component); - assertEquals(CONDITION_ID, rule.conditionId); - assertEquals(INTERRUPTION_FILTER_ZR, rule.zenMode); - assertEquals(ENABLED, rule.enabled); - assertEquals(POLICY, rule.zenPolicy); - assertEquals(CONFIG_ACTIVITY, rule.configurationActivity); - assertEquals(TYPE, rule.type); - assertEquals(ALLOW_MANUAL, rule.allowManualInvocation); - assertEquals(OWNER.getPackageName(), rule.getPkg()); - assertEquals(ICON_RES_NAME, rule.iconResName); + String ruleId = mZenModeHelper.addAutomaticZenRule(OWNER.getPackageName(), azr, + UPDATE_ORIGIN_APP, "add", CUSTOM_PKG_UID); + + ZenModeConfig.ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + + assertThat(storedRule).isNotNull(); + assertEquals(NAME, storedRule.name); + assertEquals(OWNER, storedRule.component); + assertEquals(CONDITION_ID, storedRule.conditionId); + assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode); + assertEquals(ENABLED, storedRule.enabled); + assertEquals(POLICY, storedRule.zenPolicy); + assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity); + assertEquals(TYPE, storedRule.type); + assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation); + assertEquals(OWNER.getPackageName(), storedRule.getPkg()); + assertEquals(ICON_RES_NAME, storedRule.iconResName); // Because the origin of the update is the app, we don't expect the bitmask to change. - assertEquals(0, rule.userModifiedFields); - assertEquals(TRIGGER_DESC, rule.triggerDescription); + assertEquals(0, storedRule.userModifiedFields); + assertEquals(TRIGGER_DESC, storedRule.triggerDescription); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesNameUnlessUserModified() { + public void updateAutomaticZenRule_fromApp_updatesNameUnlessUserModified() { // Add a starting rule with the name OriginalName. AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) @@ -3492,7 +3486,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("NewName"); - assertThat(rule.canUpdate()).isTrue(); // The user modifies some other field in the rule, which makes the rule as a whole not // app modifiable. @@ -3501,10 +3494,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.getUserModifiedFields()) - .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER); - assertThat(rule.canUpdate()).isFalse(); // ...but the app can still modify the name, because the name itself hasn't been modified // by the user. @@ -3524,8 +3513,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("UserProvidedName"); - assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME - | AutomaticZenRule.FIELD_INTERRUPTION_FILTER); // The app is no longer able to modify the name. azrUpdate = new AutomaticZenRule.Builder(rule) @@ -3539,7 +3526,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesBitmaskAndValueForUserOrigin() { + public void updateAutomaticZenRule_fromUser_updatesBitmaskAndValue() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setZenPolicy(new ZenPolicy.Builder().build()) @@ -3571,84 +3558,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(rule.getUserModifiedFields()) - .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER); - assertThat(rule.getZenPolicy().getUserModifiedFields()) - .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS); assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(rule.getDeviceEffects().getUserModifiedFields()) - .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); - } - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_doesNotUpdateValuesForInitUserOrigin() { - // Adds a starting rule with empty zen policies and device effects - AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) - .setInterruptionFilter(INTERRUPTION_FILTER_ALL) // Already the default, no change - .setZenPolicy(new ZenPolicy.Builder() - .allowReminders(false) - .build()) - .setDeviceEffects(new ZenDeviceEffects.Builder() - .setShouldDisplayGrayscale(false) - .build()) - .build(); - // Adds the rule using the user, to set user-modified bits. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isFalse(); - assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME); - - ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) - .allowReminders(true) - .build(); - ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects()) - .setShouldDisplayGrayscale(true) - .build(); - AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) - .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) - .setZenPolicy(policy) - .setDeviceEffects(deviceEffects) - .build(); - - // Attempts to update the rule with the AZR from origin init user. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", - Process.SYSTEM_UID); - AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - - // UPDATE_ORIGIN_INIT_USER does not change the bitmask or values if rule is user modified. - // TODO: b/318506692 - Remove once we check that INIT origins can't call add/updateAZR. - assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields()); - assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL); - assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo( - rule.getZenPolicy().getUserModifiedFields()); - assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()).isEqualTo( - ZenPolicy.STATE_DISALLOW); - assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo( - rule.getDeviceEffects().getUserModifiedFields()); - assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isFalse(); - - // Creates a new rule with the AZR from origin init user. - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", Process.SYSTEM_UID); - AutomaticZenRule newRule = mZenModeHelper.getAutomaticZenRule(newRuleId); - - // UPDATE_ORIGIN_INIT_USER does change the values if the rule is new, - // but does not update the bitmask. - assertThat(newRule.getUserModifiedFields()).isEqualTo(0); - assertThat(newRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(newRule.getZenPolicy().getUserModifiedFields()).isEqualTo(0); - assertThat(newRule.getZenPolicy().getPriorityCategoryReminders()) - .isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(newRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0); - assertThat(newRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields) + .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER); + assertThat(storedRule.zenPolicyUserModifiedFields) + .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields) + .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesValuesForSystemUiOrigin() { + public void updateAutomaticZenRule_fromSystemUi_updatesValues() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALL) @@ -3684,17 +3608,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule = mZenModeHelper.getAutomaticZenRule(ruleId); // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask. - assertThat(rule.getUserModifiedFields()).isEqualTo(0); - assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(0); assertThat(rule.getZenPolicy().getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields).isEqualTo(0); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(0); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesValuesIfRuleNotUserModified() { + public void updateAutomaticZenRule_fromApp_updatesValuesIfRuleNotUserModified() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALL) @@ -3709,7 +3635,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); ZenPolicy policy = new ZenPolicy.Builder() .allowReminders(true) @@ -3717,57 +3642,59 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) .build(); - AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) + AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(policy) .setDeviceEffects(deviceEffects) .build(); - // Since the rule is not already user modified, UPDATE_ORIGIN_UNKNOWN can modify the rule. + // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule. // The bitmask is not modified. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_UNKNOWN, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); - AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields()); - assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); - assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo( - rule.getZenPolicy().getUserModifiedFields()); - assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()) + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields).isEqualTo(0); + + assertThat(storedRule.zenMode).isEqualTo(ZEN_MODE_ALARMS); + assertThat(storedRule.zenPolicy.getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo( - rule.getDeviceEffects().getUserModifiedFields()); - assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + assertThat(storedRule.zenDeviceEffects.shouldDisplayGrayscale()).isTrue(); + assertThat(storedRule.userModifiedFields).isEqualTo(0); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(0); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0); // Creates another rule, this time from user. This will have user modified bits set. String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); - AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); - assertThat(ruleUser.canUpdate()).isFalse(); + storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser); + int ruleModifiedFields = storedRule.userModifiedFields; + int rulePolicyModifiedFields = storedRule.zenPolicyUserModifiedFields; + int ruleDeviceEffectsModifiedFields = storedRule.zenDeviceEffectsUserModifiedFields; - // Zen rule update coming from unknown origin. This cannot fully update the rule, because + // Zen rule update coming from the app again. This cannot fully update the rule, because // the rule is already considered user modified. - mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_UNKNOWN, + mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); - ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); + AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); - // UPDATE_ORIGIN_UNKNOWN can only change the value if the rule is not already user modified, + // The app can only change the value if the rule is not already user modified, // so the rule is not changed, and neither is the bitmask. assertThat(ruleUser.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL); - // Interruption Filter All is the default value, so it's not included as a modified field. - assertThat(ruleUser.getUserModifiedFields() | AutomaticZenRule.FIELD_NAME).isGreaterThan(0); - assertThat(ruleUser.getZenPolicy().getUserModifiedFields() - | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS).isGreaterThan(0); assertThat(ruleUser.getZenPolicy().getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_DISALLOW); - assertThat(ruleUser.getDeviceEffects().getUserModifiedFields() - | ZenDeviceEffects.FIELD_GRAYSCALE).isGreaterThan(0); assertThat(ruleUser.getDeviceEffects().shouldDisplayGrayscale()).isFalse(); + + storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser); + assertThat(storedRule.userModifiedFields).isEqualTo(ruleModifiedFields); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(rulePolicyModifiedFields); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( + ruleDeviceEffectsModifiedFields); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesValuesIfRuleNew() { + public void addAutomaticZenRule_updatesValues() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) @@ -3778,21 +3705,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDisplayGrayscale(true) .build()) .build(); - // Adds the rule using origin unknown, to show that a new rule is always allowed. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_UNKNOWN, "reason", Process.SYSTEM_UID); + azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // The values are modified but the bitmask is not. - assertThat(rule.canUpdate()).isTrue(); assertThat(rule.getZenPolicy().getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_ALLOW); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.canBeUpdatedByApp()).isTrue(); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_nullDeviceEffectsUpdate() { + public void updateAutomaticZenRule_nullDeviceEffectsUpdate() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setDeviceEffects(new ZenDeviceEffects.Builder().build()) @@ -3807,9 +3735,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(null) .build(); - // Zen rule update coming from unknown origin, but since the rule isn't already + // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); @@ -3819,7 +3747,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_nullPolicyUpdate() { + public void updateAutomaticZenRule_nullPolicyUpdate() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setZenPolicy(new ZenPolicy.Builder().build()) @@ -3828,16 +3756,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Set zen policy to null .setZenPolicy(null) .build(); - // Zen rule update coming from unknown origin, but since the rule isn't already + // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); @@ -3859,7 +3786,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); // Create a fully populated ZenPolicy. ZenPolicy policy = new ZenPolicy.Builder() @@ -3894,8 +3820,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { // New ZenPolicy differs from the default config assertThat(rule.getZenPolicy()).isNotNull(); assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); - assertThat(rule.canUpdate()).isFalse(); - assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo( + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS @@ -3918,7 +3846,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -3935,8 +3862,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { // New ZenDeviceEffects is used; all fields considered set, since previously were null. assertThat(rule.getDeviceEffects()).isNotNull(); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); - assertThat(rule.canUpdate()).isFalse(); - assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo( + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( ZenDeviceEffects.FIELD_GRAYSCALE); } @@ -4339,7 +4268,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); - assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).canUpdate()).isTrue(); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) @@ -4371,9 +4299,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo( ZenPolicy.STATE_ALLOW); - assertThat(finalRule.getUserModifiedFields()).isEqualTo( + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields).isEqualTo( AutomaticZenRule.FIELD_INTERRUPTION_FILTER); - assertThat(finalRule.getZenPolicy().getUserModifiedFields()).isEqualTo( + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS); // Also, we discarded the "deleted rule" since we already used it for restoration. @@ -4652,7 +4582,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, true)); @@ -4672,12 +4602,75 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZEN_MODE_ALARMS); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true)); } @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalZenModeAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setInterruptionFilter" and create and implicit rule. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // From user, update that rule's interruption filter. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setInterruptionFilter" again. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_NO_INTERRUPTIONS); + + // The app's update was ignored, and the user's update is still current, and the current + // mode is the one they chose. + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_ALARMS); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalZenModeAsImplicitZenRule_ruleCustomizedButNotFilter_updatesRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setInterruptionFilter" and create and implicit rule. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // From user, update something in that rule, but not the interruption filter. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setName("Renamed") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setInterruptionFilter" again. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_NO_INTERRUPTIONS); + + // The app's update was accepted, and the current mode is the one that they wanted. + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_NO_INTERRUPTIONS); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_NO_INTERRUPTIONS); + } + + @Test public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mZenModeHelper.mConfig.automaticRules.clear(); @@ -4747,8 +4740,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy, - UPDATE_ORIGIN_APP); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() @@ -4758,7 +4750,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowPriorityChannels(true) .build(); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, expectedZenPolicy, /* conditionActive= */ null)); @@ -4773,14 +4765,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - original, UPDATE_ORIGIN_APP); + original); // Change priorityCallSenders: contacts -> starred. Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated, - UPDATE_ORIGIN_APP); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() @@ -4790,20 +4781,87 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowPriorityChannels(true) .build(); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, expectedZenPolicy, /* conditionActive= */ null)); } @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalPolicyAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setNotificationPolicy" and create and implicit rule. + Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + + // From user, update that rule's policy. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds() + .allowAlarms(true).build(); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setZenPolicy(userUpdateZenPolicy) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setNotificationPolicy" again. + Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy); + + // The app's update was ignored, and the user's update is still current. + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_METADATA) + .containsExactly( + expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + userUpdateZenPolicy, + /* conditionActive= */ null)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalPolicyAsImplicitZenRule_ruleCustomizedButNotZenPolicy_updatesRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setNotificationPolicy" and create and implicit rule. + Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + + // From user, update something in that rule, but not the ZenPolicy. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setName("Rule renamed, not touching policy") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setNotificationPolicy" again. + Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy); + + // The app's update was applied. + ZenPolicy appsSecondZenPolicy = new ZenPolicy.Builder() + .disallowAllSounds() + .allowSystem(true) + .allowPriorityChannels(true) + .build(); + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy) + .isEqualTo(appsSecondZenPolicy); + } + + @Test public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() { mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API); mZenModeHelper.mConfig.automaticRules.clear(); withoutWtfCrash( () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, - CUSTOM_PKG_UID, new Policy(0, 0, 0), UPDATE_ORIGIN_APP)); + CUSTOM_PKG_UID, new Policy(0, 0, 0))); assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); } @@ -4816,7 +4874,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy.getAllSuppressedVisualEffects(), STATE_FALSE, CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - writtenPolicy, UPDATE_ORIGIN_APP); + writtenPolicy); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); @@ -4856,7 +4914,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(readPolicy.allowConversations()).isFalse(); } - private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS = + private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = Correspondence.transforming(zr -> { Parcel p = Parcel.obtain(); try { @@ -4864,12 +4922,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { p.setDataPosition(0); ZenRule copy = new ZenRule(p); copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; return copy; } finally { p.recycle(); } }, - "Ignoring timestamps"); + "Ignoring timestamp and userModifiedFields"); private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index 7941eb4d2090..4ed55df7775c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -645,38 +645,12 @@ public class ZenPolicyTest extends UiServiceTestCase { } @Test - public void testFromParcel() { - ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.setUserModifiedFields(10); - - ZenPolicy policy = builder.build(); - assertThat(policy.getUserModifiedFields()).isEqualTo(10); - - Parcel parcel = Parcel.obtain(); - policy.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - - ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel); - assertThat(fromParcel.getUserModifiedFields()).isEqualTo(10); - } - - @Test - public void testPolicy_userModifiedFields() { - ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.setUserModifiedFields(10); - assertThat(builder.build().getUserModifiedFields()).isEqualTo(10); - - builder.setUserModifiedFields(0); - assertThat(builder.build().getUserModifiedFields()).isEqualTo(0); - } - - @Test public void testPolicyBuilder_constructFromPolicy() { ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy policy = builder.allowRepeatCallers(true).allowAlarms(false) .showLights(true).showBadges(false) .allowPriorityChannels(true) - .setUserModifiedFields(20).build(); + .build(); ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build(); @@ -689,7 +663,6 @@ public class ZenPolicyTest extends UiServiceTestCase { assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java index 71dbc57e5065..298637266cc3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.platform.test.annotations.Presubmit; +import android.util.ArraySet; import androidx.test.filters.SmallTest; @@ -28,7 +29,6 @@ import com.android.server.wm.SensitiveContentPackages.PackageInfo; import org.junit.After; import org.junit.Test; -import java.util.Collections; import java.util.Set; /** @@ -52,18 +52,21 @@ public class SensitiveContentPackagesTest { @After public void tearDown() { - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + mSensitiveContentPackages.clearBlockedApps(); } @Test - public void setShouldBlockScreenCaptureForApp() { - Set<PackageInfo> blockedApps = + public void addBlockScreenCaptureForApps() { + ArraySet<PackageInfo> blockedApps = new ArraySet( Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), new PackageInfo(APP_PKG_1, APP_UID_2), new PackageInfo(APP_PKG_2, APP_UID_1), - new PackageInfo(APP_PKG_2, APP_UID_2)); + new PackageInfo(APP_PKG_2, APP_UID_2) + )); - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedApps); + boolean modified = mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + + assertTrue(modified); assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); @@ -79,15 +82,93 @@ public class SensitiveContentPackagesTest { } @Test - public void setShouldBlockScreenCaptureForApp_empty() { - Set<PackageInfo> blockedApps = + public void addBlockScreenCaptureForApps_addedTwice() { + ArraySet<PackageInfo> blockedApps = new ArraySet( Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), new PackageInfo(APP_PKG_1, APP_UID_2), new PackageInfo(APP_PKG_2, APP_UID_1), - new PackageInfo(APP_PKG_2, APP_UID_2)); + new PackageInfo(APP_PKG_2, APP_UID_2) + )); + + mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + boolean modified = mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + + assertFalse(modified); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3)); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3)); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3)); + } + + @Test + public void addBlockScreenCaptureForApps_withPartialPreviousPackages() { + ArraySet<PackageInfo> blockedApps = new ArraySet( + Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), + new PackageInfo(APP_PKG_1, APP_UID_2), + new PackageInfo(APP_PKG_2, APP_UID_1), + new PackageInfo(APP_PKG_2, APP_UID_2) + )); + + mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + boolean modified = mSensitiveContentPackages + .addBlockScreenCaptureForApps( + new ArraySet(Set.of(new PackageInfo(APP_PKG_3, APP_UID_1)))); + + assertTrue(modified); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3)); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3)); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3)); + } + + @Test + public void clearBlockedApps() { + ArraySet<PackageInfo> blockedApps = new ArraySet( + Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), + new PackageInfo(APP_PKG_1, APP_UID_2), + new PackageInfo(APP_PKG_2, APP_UID_1), + new PackageInfo(APP_PKG_2, APP_UID_2) + )); + + mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + boolean modified = mSensitiveContentPackages.clearBlockedApps(); + + assertTrue(modified); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3)); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3)); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3)); + } + + @Test + public void clearBlockedApps_alreadyEmpty() { + boolean modified = mSensitiveContentPackages.clearBlockedApps(); - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedApps); - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + assertFalse(modified); assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index a1cc8d5d9188..fe9d83776ad9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -115,7 +115,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; -import java.util.Collections; /** * Build/Install/Run: @@ -139,7 +138,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { @After public void tearDown() { - mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + mWm.mSensitiveContentPackages.clearBlockedApps(); } @Test @@ -824,7 +823,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - public void setShouldBlockScreenCaptureForApp() { + public void addBlockScreenCaptureForApps() { String testPackage = "test"; int ownerId1 = 20; int ownerId2 = 21; @@ -833,7 +832,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { blockedPackages.add(blockedPackage); WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); - wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); assertTrue(mWm.mSensitiveContentPackages .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); @@ -843,7 +842,47 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - public void setShouldBlockScreenCaptureForApp_emptySet_clearsCache() { + public void addBlockScreenCaptureForApps_duplicate_verifyNoRefresh() { + String testPackage = "test"; + int ownerId1 = 20; + int ownerId2 = 21; + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + + verify(mWm, times(1)).refreshScreenCaptureDisabled(); + } + + @Test + public void addBlockScreenCaptureForApps_notDuplicate_verifyRefresh() { + String testPackage = "test"; + int ownerId1 = 20; + int ownerId2 = 21; + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + PackageInfo blockedPackage2 = new PackageInfo(testPackage, ownerId2); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + ArraySet<PackageInfo> blockedPackages2 = new ArraySet(); + blockedPackages2.add(blockedPackage); + blockedPackages2.add(blockedPackage2); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + wmInternal.addBlockScreenCaptureForApps(blockedPackages2); + + assertTrue(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); + assertTrue(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId2)); + verify(mWm, times(2)).refreshScreenCaptureDisabled(); + } + + @Test + public void clearBlockedApps_clearsCache() { String testPackage = "test"; int ownerId1 = 20; PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); @@ -851,8 +890,8 @@ public class WindowManagerServiceTests extends WindowTestsBase { blockedPackages.add(blockedPackage); WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); - wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages); - wmInternal.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + wmInternal.clearBlockedApps(); assertFalse(mWm.mSensitiveContentPackages .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); @@ -860,6 +899,14 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + public void clearBlockedApps_alreadyEmpty_verifyNoRefresh() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.clearBlockedApps(); + + verify(mWm, never()).refreshScreenCaptureDisabled(); + } + + @Test public void testisLetterboxBackgroundMultiColored() { assertThat(setupLetterboxConfigurationWithBackgroundType( LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING)).isTrue(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index fb4edfacb8e3..a0562aa2f710 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -136,7 +136,7 @@ public class WindowStateTests extends WindowTestsBase { @After public void tearDown() { - mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + mWm.mSensitiveContentPackages.clearBlockedApps(); } @Test @@ -1398,7 +1398,7 @@ public class WindowStateTests extends WindowTestsBase { PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); ArraySet<PackageInfo> blockedPackages = new ArraySet(); blockedPackages.add(blockedPackage); - mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedPackages); + mWm.mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedPackages); assertTrue(window1.isSecureLocked()); assertFalse(window2.isSecureLocked()); diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java index 336bfdd0fb14..a8fd6f29f862 100644 --- a/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java +++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java @@ -35,11 +35,13 @@ import android.util.Slog; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.Keep; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.RingBuffer; import com.android.server.usage.BroadcastResponseStatsTracker.NotificationEventType; +import java.util.function.IntFunction; +import java.util.function.Supplier; + public class BroadcastResponseStatsLogger { private static final int MAX_LOG_SIZE = @@ -49,10 +51,10 @@ public class BroadcastResponseStatsLogger { @GuardedBy("mLock") private final LogBuffer mBroadcastEventsBuffer = new LogBuffer( - BroadcastEvent.class, MAX_LOG_SIZE); + BroadcastEvent::new, BroadcastEvent[]::new, MAX_LOG_SIZE); @GuardedBy("mLock") private final LogBuffer mNotificationEventsBuffer = new LogBuffer( - NotificationEvent.class, MAX_LOG_SIZE); + NotificationEvent::new, NotificationEvent[]::new, MAX_LOG_SIZE); void logBroadcastDispatchEvent(int sourceUid, @NonNull String targetPackage, UserHandle targetUser, long idForResponseEvent, @@ -96,8 +98,8 @@ public class BroadcastResponseStatsLogger { private static final class LogBuffer<T extends Data> extends RingBuffer<T> { - LogBuffer(Class<T> classType, int capacity) { - super(classType, capacity); + LogBuffer(Supplier<T> newItem, IntFunction<T[]> newBacking, int capacity) { + super(newItem, newBacking, capacity); } void logBroadcastDispatchEvent(int sourceUid, @NonNull String targetPackage, @@ -179,8 +181,7 @@ public class BroadcastResponseStatsLogger { } } - @Keep - public static final class BroadcastEvent implements Data { + private static final class BroadcastEvent implements Data { public int sourceUid; public int targetUserId; public int targetUidProcessState; @@ -200,8 +201,7 @@ public class BroadcastResponseStatsLogger { } } - @Keep - public static final class NotificationEvent implements Data { + private static final class NotificationEvent implements Data { public int type; public String packageName; public int userId; @@ -218,7 +218,7 @@ public class BroadcastResponseStatsLogger { } } - public interface Data { + private interface Data { void reset(); } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 08f719e91da9..9fc64feb4b25 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -2440,7 +2440,7 @@ public class UsageStatsService extends SystemService implements } return queryEventsHelper(userId, query.getBeginTimeMillis(), - query.getEndTimeMillis(), callingPackage, query.getEventTypeFilter()); + query.getEndTimeMillis(), callingPackage, query.getEventTypes()); } @Override diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java index 1df7012c44f8..49ad46131b0d 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java @@ -62,7 +62,8 @@ public class PhoneCallStateHandler { SubscriptionManager subscriptionManager, TelephonyManager telephonyManager, Callback callback) { - mSubscriptionManager = Objects.requireNonNull(subscriptionManager); + mSubscriptionManager = Objects.requireNonNull(subscriptionManager) + .createForAllUserProfiles(); mTelephonyManager = Objects.requireNonNull(telephonyManager); mCallback = Objects.requireNonNull(callback); mSubscriptionManager.addOnSubscriptionsChangedListener( diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 57b13e960093..e81f48280e46 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1049,8 +1049,17 @@ public class TelecomManager { public static final int PRESENTATION_UNAVAILABLE = 5; + /** + * Controls audio route for video calls. + * 0 - Use the default audio routing strategy. + * 1 - Disable the speaker. Route the audio to Headset or Bluetooth + * or Earpiece, based on the default audio routing strategy. + * @hide + */ + public static final String PROPERTY_VIDEOCALL_AUDIO_OUTPUT = "persist.radio.call.audio.output"; + /* - * Values for the adb property "persist.radio.videocall.audio.output" + * Values for the adb property "persist.radio.call.audio.output" */ /** @hide */ public static final int AUDIO_OUTPUT_ENABLE_SPEAKER = 0; diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java index 2a6ac98b4d98..86eed2f6a090 100644 --- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java +++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java @@ -601,8 +601,9 @@ public final class TelephonyPermissions { * * @return true if caller has ACCESS_LAST_KNOWN_CELL_ID permission else false. */ + @RequiresPermission(Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID) public static boolean checkLastKnownCellIdAccessPermission(Context context) { - return context.checkCallingOrSelfPermission("android.permission.ACCESS_LAST_KNOWN_CELL_ID") + return context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID) == PackageManager.PERMISSION_GRANTED; } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 73c26a3e5fc9..1badf674c8ce 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9694,6 +9694,27 @@ public class CarrierConfigManager { "remove_satellite_plmn_in_manual_network_scan_bool"; /** + * An integer key holds the time interval for refreshing or re-querying the satellite + * entitlement status from the entitlement server to ensure it is the latest. + * + * The default value is 30 days (1 month). + */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = + "satellite_entitlement_status_refresh_days_int"; + + /** + * This configuration enables device to query the entitlement server to get the satellite + * configuration. + * This will need agreement the carrier before enabling this flag. + * + * The default value is false. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = + "satellite_entitlement_supported_bool"; + + /** * Indicating whether DUN APN should be disabled when the device is roaming. In that case, * the default APN (i.e. internet) will be used for tethering. * @@ -10799,6 +10820,8 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT, CellSignalStrengthLte.USE_RSRP); sDefaults.putBoolean(KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, true); + sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 30); + sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false); sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false); sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false); diff --git a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java index c902016d2cf0..57209eb68de8 100644 --- a/telephony/java/android/telephony/SecurityAlgorithmUpdate.java +++ b/telephony/java/android/telephony/SecurityAlgorithmUpdate.java @@ -129,11 +129,15 @@ public final class SecurityAlgorithmUpdate implements Parcelable { public static final int CONNECTION_EVENT_NAS_SIGNALLING_LTE = 4; public static final int CONNECTION_EVENT_AS_SIGNALLING_LTE = 5; public static final int CONNECTION_EVENT_VOLTE_SIP = 6; - public static final int CONNECTION_EVENT_VOLTE_RTP = 7; - public static final int CONNECTION_EVENT_NAS_SIGNALLING_5G = 8; - public static final int CONNECTION_EVENT_AS_SIGNALLING_5G = 9; - public static final int CONNECTION_EVENT_VONR_SIP = 10; - public static final int CONNECTION_EVENT_VONR_RTP = 11; + public static final int CONNECTION_EVENT_VOLTE_SIP_SOS = 7; + public static final int CONNECTION_EVENT_VOLTE_RTP = 8; + public static final int CONNECTION_EVENT_VOLTE_RTP_SOS = 9; + public static final int CONNECTION_EVENT_NAS_SIGNALLING_5G = 10; + public static final int CONNECTION_EVENT_AS_SIGNALLING_5G = 11; + public static final int CONNECTION_EVENT_VONR_SIP = 12; + public static final int CONNECTION_EVENT_VONR_SIP_SOS = 13; + public static final int CONNECTION_EVENT_VONR_RTP = 14; + public static final int CONNECTION_EVENT_VONR_RTP_SOS = 15; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -141,9 +145,11 @@ public final class SecurityAlgorithmUpdate implements Parcelable { CONNECTION_EVENT_PS_SIGNALLING_GPRS, CONNECTION_EVENT_CS_SIGNALLING_3G, CONNECTION_EVENT_PS_SIGNALLING_3G, CONNECTION_EVENT_NAS_SIGNALLING_LTE, CONNECTION_EVENT_AS_SIGNALLING_LTE, CONNECTION_EVENT_VOLTE_SIP, - CONNECTION_EVENT_VOLTE_RTP, CONNECTION_EVENT_NAS_SIGNALLING_5G, + CONNECTION_EVENT_VOLTE_SIP_SOS, CONNECTION_EVENT_VOLTE_RTP, + CONNECTION_EVENT_VOLTE_RTP_SOS, CONNECTION_EVENT_NAS_SIGNALLING_5G, CONNECTION_EVENT_AS_SIGNALLING_5G, CONNECTION_EVENT_VONR_SIP, - CONNECTION_EVENT_VONR_RTP}) + CONNECTION_EVENT_VONR_SIP_SOS, CONNECTION_EVENT_VONR_RTP, + CONNECTION_EVENT_VONR_RTP_SOS}) public @interface ConnectionEvent { } @@ -169,6 +175,7 @@ public final class SecurityAlgorithmUpdate implements Parcelable { public static final int SECURITY_ALGORITHM_NEA1 = 56; public static final int SECURITY_ALGORITHM_NEA2 = 57; public static final int SECURITY_ALGORITHM_NEA3 = 58; + public static final int SECURITY_ALGORITHM_SIP_NO_IPSEC_CONFIG = 66; public static final int SECURITY_ALGORITHM_IMS_NULL = 67; public static final int SECURITY_ALGORITHM_SIP_NULL = 68; public static final int SECURITY_ALGORITHM_AES_GCM = 69; @@ -178,6 +185,7 @@ public final class SecurityAlgorithmUpdate implements Parcelable { public static final int SECURITY_ALGORITHM_AES_EDE3_CBC = 73; public static final int SECURITY_ALGORITHM_HMAC_SHA1_96 = 74; public static final int SECURITY_ALGORITHM_HMAC_MD5_96 = 75; + public static final int SECURITY_ALGORITHM_RTP = 85; public static final int SECURITY_ALGORITHM_SRTP_NULL = 86; public static final int SECURITY_ALGORITHM_SRTP_AES_COUNTER = 87; public static final int SECURITY_ALGORITHM_SRTP_AES_F8 = 88; @@ -199,15 +207,16 @@ public final class SecurityAlgorithmUpdate implements Parcelable { SECURITY_ALGORITHM_UEA2, SECURITY_ALGORITHM_EEA0, SECURITY_ALGORITHM_EEA1, SECURITY_ALGORITHM_EEA2, SECURITY_ALGORITHM_EEA3, SECURITY_ALGORITHM_NEA0, SECURITY_ALGORITHM_NEA1, SECURITY_ALGORITHM_NEA2, SECURITY_ALGORITHM_NEA3, - SECURITY_ALGORITHM_IMS_NULL, SECURITY_ALGORITHM_SIP_NULL, SECURITY_ALGORITHM_AES_GCM, + SECURITY_ALGORITHM_SIP_NO_IPSEC_CONFIG, SECURITY_ALGORITHM_IMS_NULL, + SECURITY_ALGORITHM_SIP_NULL, SECURITY_ALGORITHM_AES_GCM, SECURITY_ALGORITHM_AES_GMAC, SECURITY_ALGORITHM_AES_CBC, SECURITY_ALGORITHM_DES_EDE3_CBC, SECURITY_ALGORITHM_AES_EDE3_CBC, SECURITY_ALGORITHM_HMAC_SHA1_96, SECURITY_ALGORITHM_HMAC_MD5_96, - SECURITY_ALGORITHM_SRTP_NULL, SECURITY_ALGORITHM_SRTP_AES_COUNTER, - SECURITY_ALGORITHM_SRTP_AES_F8, SECURITY_ALGORITHM_SRTP_HMAC_SHA1, - SECURITY_ALGORITHM_ENCR_AES_GCM_16, SECURITY_ALGORITHM_ENCR_AES_CBC, - SECURITY_ALGORITHM_AUTH_HMAC_SHA2_256_128, SECURITY_ALGORITHM_UNKNOWN, - SECURITY_ALGORITHM_OTHER, SECURITY_ALGORITHM_ORYX}) + SECURITY_ALGORITHM_RTP, SECURITY_ALGORITHM_SRTP_NULL, + SECURITY_ALGORITHM_SRTP_AES_COUNTER, SECURITY_ALGORITHM_SRTP_AES_F8, + SECURITY_ALGORITHM_SRTP_HMAC_SHA1, SECURITY_ALGORITHM_ENCR_AES_GCM_16, + SECURITY_ALGORITHM_ENCR_AES_CBC, SECURITY_ALGORITHM_AUTH_HMAC_SHA2_256_128, + SECURITY_ALGORITHM_UNKNOWN, SECURITY_ALGORITHM_OTHER, SECURITY_ALGORITHM_ORYX}) public @interface SecurityAlgorithm { } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index e4ea479fb4d6..9ec5f7a77b2c 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -18483,8 +18483,8 @@ public class TelephonyManager { /** * Get last known cell identity. - * Require {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and - * com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID, otherwise throws SecurityException. + * Require appropriate permissions, otherwise throws SecurityException. + * * If there is current registered network this value will be same as the registered cell * identity. If the device goes out of service the previous cell identity is cached and * will be returned. If the cache age of the Cell identity is more than 24 hours @@ -18492,6 +18492,8 @@ public class TelephonyManager { * @return last known cell identity {@CellIdentity}. * @hide */ + @SystemApi + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) @RequiresPermission(allOf = {Manifest.permission.ACCESS_FINE_LOCATION, "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) public @Nullable CellIdentity getLastKnownCellIdentity() { @@ -18569,13 +18571,11 @@ public class TelephonyManager { * an overall "device can make voice calls" boolean, they can use {@link * ServiceState#getNetworkRegistrationInfo} to check CS registration state. * - * <p>TODO(b/215240050) In the future, this API will be removed and replaced with a new superset - * API to disentangle the "true" {@link ServiceState} meaning of "this is the connection status - * to the tower" from IMS registration state and over-the-top voice calling capabilities. - * * @hide */ @TestApi + @SystemApi + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) @RequiresPermission(Manifest.permission.BIND_TELECOM_CONNECTION_SERVICE) public void setVoiceServiceStateOverride(boolean hasService) { try { diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java index 4c37f7d3184c..b84ff2977b34 100644 --- a/telephony/java/android/telephony/ims/ImsService.java +++ b/telephony/java/android/telephony/ims/ImsService.java @@ -16,6 +16,7 @@ package android.telephony.ims; +import android.annotation.FlaggedApi; import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -46,6 +47,7 @@ import android.util.SparseBooleanArray; import com.android.ims.internal.IImsFeatureStatusCallback; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.flags.Flags; import com.android.internal.telephony.util.TelephonyUtils; import java.lang.annotation.Retention; @@ -152,12 +154,36 @@ public class ImsService extends Service { public static final long CAPABILITY_TERMINAL_BASED_CALL_WAITING = 1 << 2; /** + * This ImsService supports the capability to manage calls on multiple subscriptions at the same + * time. + * <p> + * When set, this ImsService supports managing calls on multiple subscriptions at the same time + * for all WLAN network configurations. Telephony will allow new outgoing/incoming IMS calls to + * be set up on other subscriptions while there is an ongoing call. The ImsService must also + * support managing calls on WWAN + WWAN configurations whenever the modem also reports + * simultaneous calling availability, which can be listened to using the + * {@link android.telephony.TelephonyCallback.SimultaneousCellularCallingSupportListener} API. + * Telephony will only allow additional ongoing/incoming IMS calls on another subscription to be + * set up on WWAN + WWAN configurations when the modem reports that simultaneous cellular + * calling is allowed at the current time on both subscriptions where there are ongoing calls. + * <p> + * When unset (default), this ImsService can not support calls on multiple subscriptions at the + * same time for any WLAN or WWAN configurations, so pending outgoing call placed on another + * cellular subscription while there is an ongoing call will be cancelled by Telephony. + * Similarly, any incoming call notification on another cellular subscription while there is an + * ongoing call will be rejected. + * @hide TODO: move this to system API when we have a backing implementation + CTS testing + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public static final long CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING = 1 << 3; + + /** * Used for internal correctness checks of capabilities set by the ImsService implementation and * tracks the index of the largest defined flag in the capabilities long. * @hide */ public static final long CAPABILITY_MAX_INDEX = - Long.numberOfTrailingZeros(CAPABILITY_TERMINAL_BASED_CALL_WAITING); + Long.numberOfTrailingZeros(CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING); /** * @hide diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 3b0397b6e480..70047a6feb9c 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -928,10 +928,19 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; + /** + * Satellite communication restricted by entitlement server. This can be triggered based on + * the EntitlementStatus value received from the entitlement server to enable or disable + * satellite. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; + /** @hide */ @IntDef(prefix = "SATELLITE_COMMUNICATION_RESTRICTION_REASON_", value = { SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER, - SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION + SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION, + SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteCommunicationRestrictionReason {} diff --git a/test-base/Android.bp b/test-base/Android.bp index 527159a78ebf..70a95400bd9e 100644 --- a/test-base/Android.bp +++ b/test-base/Android.bp @@ -120,12 +120,13 @@ filegroup { path: "src", } -// Make the current.txt available for use by the cts/tests/signature tests. +// Make the current.txt available for use by the cts/tests/signature and /vendor tests. // ======================================================================== filegroup { name: "android-test-base-current.txt", visibility: [ "//cts/tests/signature/api", + "//vendor:__subpackages__", ], srcs: [ "api/current.txt", diff --git a/test-mock/Android.bp b/test-mock/Android.bp index 2ff74132ffbb..f37d2d17973e 100644 --- a/test-mock/Android.bp +++ b/test-mock/Android.bp @@ -77,12 +77,13 @@ android_ravenwood_test { auto_gen_config: true, } -// Make the current.txt available for use by the cts/tests/signature tests. +// Make the current.txt available for use by the cts/tests/signature and /vendor tests. // ======================================================================== filegroup { name: "android-test-mock-current.txt", visibility: [ "//cts/tests/signature/api", + "//vendor:__subpackages__", ], srcs: [ "api/current.txt", diff --git a/test-runner/Android.bp b/test-runner/Android.bp index 13a5dac9eb38..21e09d3221ce 100644 --- a/test-runner/Android.bp +++ b/test-runner/Android.bp @@ -79,12 +79,13 @@ java_library { ], } -// Make the current.txt available for use by the cts/tests/signature tests. +// Make the current.txt available for use by the cts/tests/signature and /vendor tests. // ======================================================================== filegroup { name: "android-test-runner-current.txt", visibility: [ "//cts/tests/signature/api", + "//vendor:__subpackages__", ], srcs: [ "api/current.txt", diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt index b0ca4d230e12..79d3a10a34cb 100644 --- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt +++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt @@ -17,7 +17,10 @@ package com.android.server.wm.flicker.rotation import android.platform.test.annotations.Presubmit +import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.common.traces.component.IComponentMatcher +import android.tools.common.traces.surfaceflinger.Display import android.tools.device.apphelpers.StandardAppHelper import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest @@ -57,9 +60,8 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker @Test open fun appLayerRotates_StartingPos() { flicker.assertLayersStart { - this.entry.displays.map { display -> - this.visibleRegion(testApp).coversExactly(display.layerStackSpace) - } + val display = getDisplay(testApp) + this.visibleRegion(testApp).coversAtLeast(display.layerStackSpace) } } @@ -68,12 +70,20 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker @Test open fun appLayerRotates_EndingPos() { flicker.assertLayersEnd { - this.entry.displays.map { display -> - this.visibleRegion(testApp).coversExactly(display.layerStackSpace) - } + val display = getDisplay(testApp) + this.visibleRegion(testApp).coversAtLeast(display.layerStackSpace) } } + private fun LayerTraceEntrySubject.getDisplay(componentMatcher: IComponentMatcher): Display { + val stackId = this.layer { + componentMatcher.layerMatchesAnyOf(it) && it.isVisible + }?.layer?.stackId ?: -1 + + return this.entry.displays.firstOrNull { it.layerStackId == stackId } + ?: error("Unable to find visible layer for $componentMatcher") + } + override fun cujCompleted() { super.cujCompleted() appLayerRotates_StartingPos() diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java index e3988cd20199..c44d9435d203 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java @@ -68,7 +68,7 @@ public class ActivityEmbeddingSecondaryActivity extends Activity { // This triggers a recalcuation of splitatributes. ActivityEmbeddingController .getInstance(ActivityEmbeddingSecondaryActivity.this) - .invalidateTopVisibleActivityStacks(); + .invalidateVisibleActivityStacks(); } }); findViewById(R.id.secondary_enter_pip_button).setOnClickListener( diff --git a/tests/testables/src/android/testing/TestableContext.java b/tests/testables/src/android/testing/TestableContext.java index 0f04d6ae1721..fa8fdfac6813 100644 --- a/tests/testables/src/android/testing/TestableContext.java +++ b/tests/testables/src/android/testing/TestableContext.java @@ -62,8 +62,9 @@ import java.util.ArrayList; */ public class TestableContext extends ContextWrapper implements TestRule { - private final TestableContentResolver mTestableContentResolver; - private final TestableSettingsProvider mSettingsProvider; + private TestableContentResolver mTestableContentResolver; + private TestableSettingsProvider mSettingsProvider; + private RuntimeException mSettingsProviderFailure; private ArrayList<MockServiceResolver> mMockServiceResolvers; private ArrayMap<String, Object> mMockSystemServices; @@ -83,12 +84,24 @@ public class TestableContext extends ContextWrapper implements TestRule { public TestableContext(Context base, LeakCheck check) { super(base); - mTestableContentResolver = new TestableContentResolver(base); - ContentProviderClient settings = base.getContentResolver() - .acquireContentProviderClient(Settings.AUTHORITY); - mSettingsProvider = TestableSettingsProvider.getFakeSettingsProvider(settings); - mTestableContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider); - mSettingsProvider.clearValuesAndCheck(TestableContext.this); + + // Configure TestableSettingsProvider when possible; if we fail to initialize some + // underlying infrastructure then remember the error and report it later when a test + // attempts to interact with it + try { + ContentProviderClient settings = base.getContentResolver() + .acquireContentProviderClient(Settings.AUTHORITY); + mSettingsProvider = TestableSettingsProvider.getFakeSettingsProvider(settings); + mTestableContentResolver = new TestableContentResolver(base); + mTestableContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider); + mSettingsProvider.clearValuesAndCheck(TestableContext.this); + mSettingsProviderFailure = null; + } catch (Throwable t) { + mTestableContentResolver = null; + mSettingsProvider = null; + mSettingsProviderFailure = new RuntimeException( + "Failed to initialize TestableSettingsProvider", t); + } mReceiver = check != null ? check.getTracker("receiver") : null; mService = check != null ? check.getTracker("service") : null; mComponent = check != null ? check.getTracker("component") : null; @@ -171,11 +184,17 @@ public class TestableContext extends ContextWrapper implements TestRule { } TestableSettingsProvider getSettingsProvider() { + if (mSettingsProviderFailure != null) { + throw mSettingsProviderFailure; + } return mSettingsProvider; } @Override public TestableContentResolver getContentResolver() { + if (mSettingsProviderFailure != null) { + throw mSettingsProviderFailure; + } return mTestableContentResolver; } @@ -515,12 +534,16 @@ public class TestableContext extends ContextWrapper implements TestRule { return new TestWatcher() { @Override protected void succeeded(Description description) { - mSettingsProvider.clearValuesAndCheck(TestableContext.this); + if (mSettingsProvider != null) { + mSettingsProvider.clearValuesAndCheck(TestableContext.this); + } } @Override protected void failed(Throwable e, Description description) { - mSettingsProvider.clearValuesAndCheck(TestableContext.this); + if (mSettingsProvider != null) { + mSettingsProvider.clearValuesAndCheck(TestableContext.this); + } } }.apply(base, description); } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 82e40b1eee6b..60c25b75d2e9 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -32,6 +32,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -76,7 +77,7 @@ public class TestableLooper { } private TestableLooper(TestLooperManager wrapper, Looper l) { - mQueueWrapper = wrapper; + mQueueWrapper = Objects.requireNonNull(wrapper); setupQueue(l); } @@ -282,65 +283,94 @@ public class TestableLooper { return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l); } - private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>(); + private static final Map<Object, TestableLooperHolder> sLoopers = new ArrayMap<>(); /** * For use with {@link RunWithLooper}, used to get the TestableLooper that was * automatically created for this test. */ public static TestableLooper get(Object test) { - return sLoopers.get(test); + final TestableLooperHolder looperHolder = sLoopers.get(test); + return (looperHolder != null) ? looperHolder.mTestableLooper : null; } public static void remove(Object test) { sLoopers.remove(test); } - static class LooperFrameworkMethod extends FrameworkMethod { + /** + * Holder object that contains {@link TestableLooper} so that its initialization can be + * deferred until a test case is actually run, instead of forcing it to be created at + * {@link FrameworkMethod} construction time. + * + * This deferral is important because some test environments may configure + * {@link Looper#getMainLooper()} as part of a {@code Rule} instead of assuming it's globally + * initialized and unconditionally available. + */ + private static class TestableLooperHolder { + private final boolean mSetAsMain; + private final Object mTest; + + private TestableLooper mTestableLooper; + private Looper mLooper; + private Handler mHandler; private HandlerThread mHandlerThread; - private final TestableLooper mTestableLooper; - private final Looper mLooper; - private final Handler mHandler; + public TestableLooperHolder(boolean setAsMain, Object test) { + mSetAsMain = setAsMain; + mTest = test; + } - public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) { - super(base.getMethod()); + public void ensureInit() { + if (mLooper != null) return; try { - mLooper = setAsMain ? Looper.getMainLooper() : createLooper(); + mLooper = mSetAsMain ? Looper.getMainLooper() : createLooper(); mTestableLooper = new TestableLooper(mLooper, false); - if (!setAsMain) { - mTestableLooper.getLooper().getThread().setName(test.getClass().getName()); + if (!mSetAsMain) { + mTestableLooper.getLooper().getThread().setName(mTest.getClass().getName()); } } catch (Exception e) { throw new RuntimeException(e); } - sLoopers.put(test, mTestableLooper); mHandler = new Handler(mLooper); } - public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) { + private Looper createLooper() { + // TODO: Find way to share these. + mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); + mHandlerThread.start(); + return mHandlerThread.getLooper(); + } + } + + static class LooperFrameworkMethod extends FrameworkMethod { + private TestableLooperHolder mLooperHolder; + + public LooperFrameworkMethod(FrameworkMethod base, TestableLooperHolder looperHolder) { super(base.getMethod()); - mLooper = other.mLooper; - mTestableLooper = other; - mHandler = Handler.createAsync(mLooper); + mLooperHolder = looperHolder; } public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) { - if (sLoopers.containsKey(test)) { - return new LooperFrameworkMethod(sLoopers.get(test), base); + TestableLooperHolder looperHolder = sLoopers.get(test); + if (looperHolder == null) { + looperHolder = new TestableLooperHolder(setAsMain, test); + sLoopers.put(test, looperHolder); } - return new LooperFrameworkMethod(base, setAsMain, test); + return new LooperFrameworkMethod(base, looperHolder); } @Override public Object invokeExplosively(Object target, Object... params) throws Throwable { - if (Looper.myLooper() == mLooper) { + mLooperHolder.ensureInit(); + if (Looper.myLooper() == mLooperHolder.mLooper) { // Already on the right thread from another statement, just execute then. return super.invokeExplosively(target, params); } - boolean set = mTestableLooper.mQueueWrapper == null; + boolean set = mLooperHolder.mTestableLooper.mQueueWrapper == null; if (set) { - mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper); + mLooperHolder.mTestableLooper.mQueueWrapper = acquireLooperManager( + mLooperHolder.mLooper); } try { Object[] ret = new Object[1]; @@ -352,11 +382,11 @@ public class TestableLooper { throw new LooperException(throwable); } }; - Message m = Message.obtain(mHandler, execute); + Message m = Message.obtain(mLooperHolder.mHandler, execute); // Dispatch our message. try { - mTestableLooper.mQueueWrapper.execute(m); + mLooperHolder.mTestableLooper.mQueueWrapper.execute(m); } catch (LooperException e) { throw e.getSource(); } catch (RuntimeException re) { @@ -373,27 +403,20 @@ public class TestableLooper { return ret[0]; } finally { if (set) { - mTestableLooper.mQueueWrapper.release(); - mTestableLooper.mQueueWrapper = null; - if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { + mLooperHolder.mTestableLooper.mQueueWrapper.release(); + mLooperHolder.mTestableLooper.mQueueWrapper = null; + if (HOLD_MAIN_THREAD && mLooperHolder.mLooper == Looper.getMainLooper()) { TestableInstrumentation.releaseMain(); } } } } - private Looper createLooper() { - // TODO: Find way to share these. - mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); - mHandlerThread.start(); - return mHandlerThread.getLooper(); - } - @Override protected void finalize() throws Throwable { super.finalize(); - if (mHandlerThread != null) { - mHandlerThread.quit(); + if (mLooperHolder.mHandlerThread != null) { + mLooperHolder.mHandlerThread.quit(); } } diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index f05611048caa..1a82021bce71 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -381,9 +381,9 @@ class PackageFlattener { return true; } - bool FlattenTypeSpec(const ResourceTableTypeView& type, - const std::vector<ResourceTableEntryView>& sorted_entries, - BigBuffer* buffer) { + ResTable_typeSpec* FlattenTypeSpec(const ResourceTableTypeView& type, + const std::vector<ResourceTableEntryView>& sorted_entries, + BigBuffer* buffer) { ChunkWriter type_spec_writer(buffer); ResTable_typeSpec* spec_header = type_spec_writer.StartChunk<ResTable_typeSpec>(RES_TABLE_TYPE_SPEC_TYPE); @@ -391,7 +391,7 @@ class PackageFlattener { if (sorted_entries.empty()) { type_spec_writer.Finish(); - return true; + return spec_header; } // We can't just take the size of the vector. There may be holes in the @@ -427,7 +427,7 @@ class PackageFlattener { } } type_spec_writer.Finish(); - return true; + return spec_header; } bool FlattenTypes(BigBuffer* buffer) { @@ -450,7 +450,8 @@ class PackageFlattener { expected_type_id++; type_pool_.MakeRef(type.named_type.to_string()); - if (!FlattenTypeSpec(type, type.entries, buffer)) { + const auto type_spec_header = FlattenTypeSpec(type, type.entries, buffer); + if (!type_spec_header) { return false; } @@ -511,6 +512,10 @@ class PackageFlattener { return false; } } + + // And now we can update the type entries count in the typeSpec header. + type_spec_header->typesCount = android::util::HostToDevice16(uint16_t(std::min<uint32_t>( + config_to_entry_list_map.size(), std::numeric_limits<uint16_t>::max()))); } return true; } diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/libcore-fake/com/android/okhttp/internalandroidapi/Dns.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/libcore-fake/com/android/okhttp/internalandroidapi/Dns.java new file mode 100644 index 000000000000..379c4ae8a059 --- /dev/null +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/libcore-fake/com/android/okhttp/internalandroidapi/Dns.java @@ -0,0 +1,36 @@ +/* + * 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.okhttp.internalandroidapi; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +/** + * A domain name service that resolves IP addresses for host names. + * @hide + * @hide This class is not part of the Android public SDK API + */ +public interface Dns { + /** + * Returns the IP addresses of {@code hostname}, in the order they should + * be attempted. + * + * @hide + */ + List<InetAddress> lookup(String hostname) throws UnknownHostException; +} |