diff options
486 files changed, 13464 insertions, 3126 deletions
diff --git a/Android.bp b/Android.bp index b139b7e50676..f3b2ebb4fc17 100644 --- a/Android.bp +++ b/Android.bp @@ -218,6 +218,7 @@ 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", @@ -664,6 +665,7 @@ java_library { lint: { baseline_filename: "lint-baseline.xml", }, + apex_available: ["com.android.wifi"], } filegroup { diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp index e7adf203334e..d03bbd249b00 100644 --- a/ProtoLibraries.bp +++ b/ProtoLibraries.bp @@ -34,7 +34,6 @@ gensrcs { ":ipconnectivity-proto-src", ":libstats_atom_enum_protos", ":libstats_atom_message_protos", - ":libtombstone_proto-src", "core/proto/**/*.proto", "libs/incident/**/*.proto", ], diff --git a/STABILITY_OWNERS b/STABILITY_OWNERS new file mode 100644 index 000000000000..a7ecb4dfdd44 --- /dev/null +++ b/STABILITY_OWNERS @@ -0,0 +1,2 @@ +gaillard@google.com + diff --git a/WEAR_OWNERS b/WEAR_OWNERS index 4f3bc27c380f..4127f996da03 100644 --- a/WEAR_OWNERS +++ b/WEAR_OWNERS @@ -4,3 +4,9 @@ yeabkal@google.com adsule@google.com andriyn@google.com yfz@google.com +con@google.com +leetodd@google.com +sadrul@google.com +rwmyers@google.com +nalmalki@google.com +shijianli@google.com diff --git a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java index b7460cd6ead4..fa4387b405c9 100644 --- a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java +++ b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java @@ -433,6 +433,17 @@ public class SQLiteDatabasePerfTest { performMultithreadedReadWriteTest(); } + /** + * This test measures a multi-threaded read-write environment where there are 2 readers and + * 1 writer in the database using WAL journal mode and NORMAL syncMode. + */ + @Test + public void testMultithreadedReadWriteWithWalNormal() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL); + insertT1TestDataSet(); + performMultithreadedReadWriteTest(); + } + private void doReadLoop(int totalIterations) { Random rnd = new Random(0); int currentIteration = 0; @@ -472,7 +483,6 @@ public class SQLiteDatabasePerfTest { } private void doUpdateLoop(int totalIterations) { - SQLiteDatabase db = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null); Random rnd = new Random(0); int i = 0; ContentValues cv = new ContentValues(); @@ -485,24 +495,12 @@ public class SQLiteDatabasePerfTest { cv.put("COL_B", "UpdatedValue"); cv.put("COL_C", i); argArray[0] = String.valueOf(id); - db.update("T1", cv, "_ID=?", argArray); + mDatabase.update("T1", cv, "_ID=?", argArray); i++; android.os.Trace.endSection(); } } - /** - * This test measures a multi-threaded read-write environment where there are 2 readers and - * 1 writer in the database using WAL journal mode and NORMAL syncMode. - */ - @Test - public void testMultithreadedReadWriteWithWalNormal() { - recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL); - insertT1TestDataSet(); - - performMultithreadedReadWriteTest(); - } - private void performMultithreadedReadWriteTest() { int totalBGIterations = 10000; // Writer - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations @@ -555,4 +553,3 @@ public class SQLiteDatabasePerfTest { createOrOpenTestDatabase(journalMode, syncMode); } } - diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 5a32a02ca8ce..abf80089b985 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -115,6 +115,7 @@ import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemClock; +import android.os.SystemProperties; import android.os.ThreadLocalWorkSource; import android.os.Trace; import android.os.UserHandle; @@ -229,6 +230,9 @@ public class AlarmManagerService extends SystemService { private static final long TEMPORARY_QUOTA_DURATION = INTERVAL_DAY; + // System property read on some device configurations to initialize time properly. + private static final String TIMEOFFSET_PROPERTY = "persist.sys.time.offset"; + private final Intent mBackgroundIntent = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND); @@ -2142,6 +2146,9 @@ public class AlarmManagerService extends SystemService { // "GMT" if the ID is unrecognized). The parameter ID is used here rather than // newZone.getId(). It will be rejected if it is invalid. timeZoneWasChanged = SystemTimeZone.setTimeZoneId(tzId, confidence, logInfo); + + final int gmtOffset = newZone.getOffset(mInjector.getCurrentTimeMillis()); + SystemProperties.set(TIMEOFFSET_PROPERTY, String.valueOf(gmtOffset)); } // Clear the default time zone in the system server process. This forces the next call diff --git a/api/Android.bp b/api/Android.bp index 363197a54fac..1686943d08ca 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -444,3 +444,12 @@ genrule { targets: ["droid"], }, } + +phony_rule { + name: "checkapi", + phony_deps: [ + "frameworks-base-api-current-compat", + "frameworks-base-api-system-current-compat", + "frameworks-base-api-module-lib-current-compat", + ], +} diff --git a/api/Android.mk b/api/Android.mk deleted file mode 100644 index ce5f995033c5..000000000000 --- a/api/Android.mk +++ /dev/null @@ -1,2 +0,0 @@ -.PHONY: checkapi -checkapi: frameworks-base-api-current-compat frameworks-base-api-system-current-compat frameworks-base-api-module-lib-current-compat diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 50c9fd3ec499..ef1fa6097056 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -912,7 +912,7 @@ droidstubs { } // This module can be built with: -// m out/soong/.intermediates/frameworks/base/api_versions_module_lib/android_common/metalava/api-versions.xml +// m out/soong/.intermediates/frameworks/base/api/api_versions_module_lib/android_common/metalava/api-versions.xml droidstubs { name: "api_versions_module_lib", srcs: [":android_module_stubs_current_with_test_libs{.jar}"], diff --git a/core/api/current.txt b/core/api/current.txt index c7b921c8f6d5..6469e893e6be 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -102,6 +102,7 @@ package android { field public static final String FOREGROUND_SERVICE_HEALTH = "android.permission.FOREGROUND_SERVICE_HEALTH"; field public static final String FOREGROUND_SERVICE_LOCATION = "android.permission.FOREGROUND_SERVICE_LOCATION"; field public static final String FOREGROUND_SERVICE_MEDIA_PLAYBACK = "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"; + field @FlaggedApi("android.content.pm.introduce_media_processing_type") public static final String FOREGROUND_SERVICE_MEDIA_PROCESSING = "android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"; field public static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"; field public static final String FOREGROUND_SERVICE_MICROPHONE = "android.permission.FOREGROUND_SERVICE_MICROPHONE"; field public static final String FOREGROUND_SERVICE_PHONE_CALL = "android.permission.FOREGROUND_SERVICE_PHONE_CALL"; @@ -5315,6 +5316,7 @@ 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(); @@ -12368,7 +12370,6 @@ package android.content.pm { public final class ModuleInfo implements android.os.Parcelable { method public int describeContents(); - method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @NonNull public java.util.Collection<java.lang.String> getApkInApexPackageNames(); method @Nullable public CharSequence getName(); method @Nullable public String getPackageName(); method public boolean isHidden(); @@ -12817,7 +12818,7 @@ package android.content.pm { method public boolean isPackageSuspended(); method @CheckResult public abstract boolean isPermissionRevokedByPolicy(@NonNull String, @NonNull String); method public abstract boolean isSafeMode(); - method @FlaggedApi("android.content.pm.get_package_info") @WorkerThread public <T> T parseAndroidManifest(@NonNull String, @NonNull java.util.function.Function<android.content.res.XmlResourceParser,T>) throws java.io.IOException; + method @FlaggedApi("android.content.pm.get_package_info") @WorkerThread public <T> T parseAndroidManifest(@NonNull java.io.File, @NonNull java.util.function.Function<android.content.res.XmlResourceParser,T>) throws java.io.IOException; method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryActivityProperty(@NonNull String); method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryApplicationProperty(@NonNull String); method @NonNull public abstract java.util.List<android.content.pm.ResolveInfo> queryBroadcastReceivers(@NonNull android.content.Intent, int); @@ -13287,6 +13288,7 @@ package android.content.pm { field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2 + field @FlaggedApi("android.content.pm.introduce_media_processing_type") @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING = 8192; // 0x2000 field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32; // 0x20 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_MICROPHONE}, anyOf={android.Manifest.permission.CAPTURE_AUDIO_OUTPUT, android.Manifest.permission.RECORD_AUDIO}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80 field @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0 @@ -18615,6 +18617,7 @@ package android.hardware { } public final class SyncFence implements java.lang.AutoCloseable android.os.Parcelable { + ctor @FlaggedApi("com.android.window.flags.sdk_desired_present_time") public SyncFence(@NonNull android.hardware.SyncFence); method public boolean await(@NonNull java.time.Duration); method public boolean awaitForever(); method public void close(); @@ -26291,6 +26294,7 @@ package android.media.session { field public static final int STATE_FAST_FORWARDING = 4; // 0x4 field public static final int STATE_NONE = 0; // 0x0 field public static final int STATE_PAUSED = 2; // 0x2 + field @FlaggedApi("com.android.media.flags.enable_notifying_activity_manager_with_media_session_status_change") public static final int STATE_PLAYBACK_SUPPRESSED = 12; // 0xc field public static final int STATE_PLAYING = 3; // 0x3 field public static final int STATE_REWINDING = 5; // 0x5 field public static final int STATE_SKIPPING_TO_NEXT = 10; // 0xa @@ -28955,7 +28959,7 @@ package android.nfc.cardemulation { method public boolean supportsAidPrefixRegistration(); method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean unsetOffHostForService(@NonNull android.content.ComponentName); method public boolean unsetPreferredService(android.app.Activity); - field public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; + field @Deprecated public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; field public static final String CATEGORY_OTHER = "other"; field public static final String CATEGORY_PAYMENT = "payment"; field public static final String EXTRA_CATEGORY = "category"; @@ -51795,6 +51799,7 @@ package android.view { public static class SurfaceControl.Transaction implements java.io.Closeable android.os.Parcelable { ctor public SurfaceControl.Transaction(); method @NonNull public android.view.SurfaceControl.Transaction addTransactionCommittedListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.SurfaceControl.TransactionCommittedListener); + method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction addTransactionCompletedListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.SurfaceControl.TransactionStats>); method public void apply(); method @NonNull public android.view.SurfaceControl.Transaction clearFrameRate(@NonNull android.view.SurfaceControl); method @NonNull public android.view.SurfaceControl.Transaction clearTrustedPresentationCallback(@NonNull android.view.SurfaceControl); @@ -51811,9 +51816,11 @@ 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 @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); + method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setFrameTimeline(long); method @Deprecated @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int); method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int); method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean); @@ -51829,6 +51836,11 @@ package android.view { method public void onTransactionCommitted(); } + @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") @NonNull public android.hardware.SyncFence getPresentFence(); + } + public static final class SurfaceControl.TrustedPresentationThresholds { ctor public SurfaceControl.TrustedPresentationThresholds(@FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @IntRange(from=1) int); } @@ -53974,6 +53986,7 @@ package android.view { field public static final String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS"; field public static final String PROPERTY_COMPAT_ENABLE_FAKE_FOCUS = "android.window.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS"; field public static final String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; + field @FlaggedApi("com.android.window.flags.supports_multi_instance_system_ui") public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"; } public static class WindowManager.BadTokenException extends java.lang.RuntimeException { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 9077d02be8be..2898705b75d4 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -475,6 +475,7 @@ package android { field public static final int config_defaultNotes = 17039429; // 0x1040045 field @FlaggedApi("android.permission.flags.retail_demo_role_enabled") public static final int config_defaultRetailDemo; field public static final int config_defaultSms = 17039396; // 0x1040024 + field @FlaggedApi("android.permission.flags.wallet_role_enabled") public static final int config_defaultWallet; field public static final int config_devicePolicyManagement = 17039421; // 0x104003d field public static final int config_feedbackIntentExtraKey = 17039391; // 0x104001f field public static final int config_feedbackIntentNameKey = 17039392; // 0x1040020 @@ -5747,7 +5748,7 @@ package android.hardware.radio { method public void addOnCompleteListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.OnCompleteListener); method public void addOnCompleteListener(@NonNull android.hardware.radio.ProgramList.OnCompleteListener); method public void close(); - method @Deprecated @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier); + method @Nullable public android.hardware.radio.RadioManager.ProgramInfo get(@NonNull android.hardware.radio.ProgramSelector.Identifier); method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull public java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramInfos(@NonNull android.hardware.radio.ProgramSelector.Identifier); method public void registerListCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.radio.ProgramList.ListCallback); method public void registerListCallback(@NonNull android.hardware.radio.ProgramList.ListCallback); @@ -5799,7 +5800,7 @@ package android.hardware.radio { field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5 field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5 field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa - field @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb + field public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9 field public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; // 0x3 field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int IDENTIFIER_TYPE_HD_STATION_LOCATION = 15; // 0xf @@ -5807,8 +5808,8 @@ package android.hardware.radio { field @Deprecated public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4; // 0x4 field public static final int IDENTIFIER_TYPE_INVALID = 0; // 0x0 field public static final int IDENTIFIER_TYPE_RDS_PI = 2; // 0x2 - field @Deprecated public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd - field @Deprecated public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc + field public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; // 0xd + field public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; // 0xc field public static final int IDENTIFIER_TYPE_VENDOR_END = 1999; // 0x7cf field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = 1999; // 0x7cf field @Deprecated public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = 1000; // 0x3e8 @@ -5861,7 +5862,7 @@ package android.hardware.radio { field public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8; // 0x8 field public static final int CONFIG_DAB_FM_LINKING = 7; // 0x7 field public static final int CONFIG_DAB_FM_SOFT_LINKING = 9; // 0x9 - field @Deprecated public static final int CONFIG_FORCE_ANALOG = 2; // 0x2 + field public static final int CONFIG_FORCE_ANALOG = 2; // 0x2 field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_AM = 11; // 0xb field @FlaggedApi("android.hardware.radio.hd_radio_improved") public static final int CONFIG_FORCE_ANALOG_FM = 10; // 0xa field public static final int CONFIG_FORCE_DIGITAL = 3; // 0x3 @@ -9967,6 +9968,7 @@ package android.nfc.cardemulation { } public final class CardEmulation { + method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public android.nfc.cardemulation.ApduServiceInfo getPreferredPaymentService(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 572be192fb3e..2e22071d72ad 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -284,6 +284,16 @@ 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); @@ -1872,6 +1882,7 @@ package android.media { method public void setRampingRingerEnabled(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setRs2Value(float); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setTestDeviceConnectionState(@NonNull android.media.AudioDeviceAttributes, boolean); + method @FlaggedApi("android.media.audio.focus_exclusive_with_recording") @RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE) public boolean shouldNotificationSoundPlay(@NonNull android.media.AudioAttributes); } public static final class AudioRecord.MetricsConstants { @@ -2333,6 +2344,9 @@ package android.os { field public static final int CPU_LOAD_RESET = 2; // 0x2 field public static final int CPU_LOAD_RESUME = 3; // 0x3 field public static final int CPU_LOAD_UP = 0; // 0x0 + field @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static final int GPU_LOAD_DOWN = 6; // 0x6 + field @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static final int GPU_LOAD_RESET = 7; // 0x7 + field @FlaggedApi("android.os.adpf_gpu_report_actual_work_duration") public static final int GPU_LOAD_UP = 5; // 0x5 } public final class PowerManager { @@ -3007,6 +3021,49 @@ 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); + } + } package android.service.quickaccesswallet { diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt index bf26bd0a0ec6..5e904ef947c8 100644 --- a/core/api/test-lint-baseline.txt +++ b/core/api/test-lint-baseline.txt @@ -535,6 +535,10 @@ MissingNullability: android.widget.ImageView#isDefaultFocusHighlightNeeded(andro Missing nullability on parameter `foreground` in method `isDefaultFocusHighlightNeeded` +OptionalBuilderConstructorArgument: android.service.notification.ZenPolicy.Builder#Builder(android.service.notification.ZenPolicy) parameter #0: + Builder constructor arguments must be mandatory (i.e. not @Nullable): parameter policy in android.service.notification.ZenPolicy.Builder(android.service.notification.ZenPolicy policy) + + ProtectedMember: android.app.AppDetailsActivity#onCreate(android.os.Bundle): Protected methods not allowed; must be public: method android.app.AppDetailsActivity.onCreate(android.os.Bundle)} ProtectedMember: android.view.ViewGroup#resetResolvedDrawables(): @@ -2143,6 +2147,8 @@ UnflaggedApi: android.service.notification.NotificationRankingUpdate#PARCELABLE_ New API must be flagged with @FlaggedApi: field android.service.notification.NotificationRankingUpdate.PARCELABLE_WRITE_RETURN_VALUE UnflaggedApi: android.service.notification.NotificationRankingUpdate#isFdNotNullAndClosed(): New API must be flagged with @FlaggedApi: method android.service.notification.NotificationRankingUpdate.isFdNotNullAndClosed() +UnflaggedApi: android.service.notification.ZenPolicy.Builder#Builder(android.service.notification.ZenPolicy): + New API must be flagged with @FlaggedApi: constructor android.service.notification.ZenPolicy.Builder(android.service.notification.ZenPolicy) UnflaggedApi: android.telephony.TelephonyManager#HAL_SERVICE_SATELLITE: New API must be flagged with @FlaggedApi: field android.telephony.TelephonyManager.HAL_SERVICE_SATELLITE UnflaggedApi: android.telephony.ims.feature.MmTelFeature.MmTelCapabilities: diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index b4a6955325a3..845a346be593 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -1311,8 +1311,9 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim if (!node.mEnded) { float durationScale = ValueAnimator.getDurationScale(); durationScale = durationScale == 0 ? 1 : durationScale; - node.mEnded = node.mAnimation.pulseAnimationFrame( - (long) (animPlayTime * durationScale)); + if (node.mAnimation.pulseAnimationFrame((long) (animPlayTime * durationScale))) { + node.mEnded = true; + } } } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 287d2bd9e6a7..34c44f9489d5 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -131,6 +131,7 @@ import dalvik.system.VMRuntime; import libcore.util.EmptyArray; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; @@ -4038,11 +4039,11 @@ public class ApplicationPackageManager extends PackageManager { } @Override - public <T> T parseAndroidManifest(@NonNull String apkFilePath, + public <T> T parseAndroidManifest(@NonNull File apkFile, @NonNull Function<XmlResourceParser, T> parserFunction) throws IOException { - Objects.requireNonNull(apkFilePath, "apkFilePath cannot be null"); + Objects.requireNonNull(apkFile, "apkFile cannot be null"); Objects.requireNonNull(parserFunction, "parserFunction cannot be null"); - try (XmlResourceParser xmlResourceParser = getAndroidManifestParser(apkFilePath)) { + try (XmlResourceParser xmlResourceParser = getAndroidManifestParser(apkFile)) { return parserFunction.apply(xmlResourceParser); } catch (IOException e) { Log.w(TAG, "Failed to get the android manifest parser", e); @@ -4050,11 +4051,11 @@ public class ApplicationPackageManager extends PackageManager { } } - private static XmlResourceParser getAndroidManifestParser(@NonNull String apkFilePath) + private static XmlResourceParser getAndroidManifestParser(@NonNull File apkFile) throws IOException { ApkAssets apkAssets = null; try { - apkAssets = ApkAssets.loadFromPath(apkFilePath); + apkAssets = ApkAssets.loadFromPath(apkFile.getAbsolutePath()); return apkAssets.openXml(ApkLiteParseUtils.ANDROID_MANIFEST_FILENAME); } finally { if (apkAssets != null) { diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index f9ab55e00dc6..5b354fc3b9ed 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -23,6 +23,7 @@ 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; @@ -35,6 +36,7 @@ import android.view.WindowInsetsController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Objects; /** @@ -111,6 +113,30 @@ 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()}. + * @hide + */ + @IntDef(flag = true, prefix = { "FIELD_" }, value = { + FIELD_NAME, + FIELD_INTERRUPTION_FILTER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ModifiableField {} + + /** + * @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; private String name; private @InterruptionFilter int interruptionFilter; @@ -120,12 +146,14 @@ public final class AutomaticZenRule implements Parcelable { private long creationTime; private ZenPolicy mZenPolicy; private ZenDeviceEffects mDeviceEffects; + // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined. private boolean mModified = false; private String mPkg; - private int mType = TYPE_UNKNOWN; + private int mType = Flags.modesApi() ? TYPE_UNKNOWN : 0; 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 @@ -228,6 +256,7 @@ public final class AutomaticZenRule implements Parcelable { mIconResId = source.readInt(); mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); mType = source.readInt(); + mUserModifiedFields = source.readInt(); } } @@ -278,6 +307,8 @@ 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. public boolean isModified() { return mModified; } @@ -475,6 +506,32 @@ 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; @@ -503,6 +560,7 @@ public final class AutomaticZenRule implements Parcelable { dest.writeInt(mIconResId); dest.writeString(mTriggerDescription); dest.writeInt(mType); + dest.writeInt(mUserModifiedFields); } } @@ -524,12 +582,26 @@ public final class AutomaticZenRule implements Parcelable { .append(",allowManualInvocation=").append(mAllowManualInvocation) .append(",iconResId=").append(mIconResId) .append(",triggerDescription=").append(mTriggerDescription) - .append(",type=").append(mType); + .append(",type=").append(mType) + .append(",userModifiedFields=") + .append(modifiedFieldsToString(mUserModifiedFields)); } return sb.append(']').toString(); } + @FlaggedApi(Flags.FLAG_MODES_API) + private String modifiedFieldsToString(int bitmask) { + ArrayList<String> modified = new ArrayList<>(); + if ((bitmask & FIELD_NAME) != 0) { + modified.add("FIELD_NAME"); + } + if ((bitmask & FIELD_INTERRUPTION_FILTER) != 0) { + modified.add("FIELD_INTERRUPTION_FILTER"); + } + return "{" + String.join(",", modified) + "}"; + } + @Override public boolean equals(@Nullable Object o) { if (!(o instanceof AutomaticZenRule)) return false; @@ -551,7 +623,8 @@ public final class AutomaticZenRule implements Parcelable { && other.mAllowManualInvocation == mAllowManualInvocation && other.mIconResId == mIconResId && Objects.equals(other.mTriggerDescription, mTriggerDescription) - && other.mType == mType; + && other.mType == mType + && other.mUserModifiedFields == mUserModifiedFields; } return finalEquals; } @@ -561,7 +634,8 @@ 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); + mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType, + mUserModifiedFields); } return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mModified, creationTime, mPkg); @@ -630,6 +704,7 @@ 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(); @@ -646,6 +721,7 @@ 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) { @@ -772,6 +848,19 @@ 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); @@ -782,6 +871,7 @@ 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/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index ac9c497f2a36..d1e517bbd03c 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -30,6 +30,7 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; @@ -577,6 +578,26 @@ public abstract class ForegroundServiceTypePolicy { ); /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PROCESSING = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission( + Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING) + }, true), + null /* anyOfPermissions */, + null /* permissionEnforcementFlag */, + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ + ); + + /** * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}. * * @hide @@ -1331,6 +1352,8 @@ public abstract class ForegroundServiceTypePolicy { FGS_TYPE_POLICY_SYSTEM_EXEMPTED); mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, FGS_TYPE_POLICY_SHORT_SERVICE); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, + FGS_TYPE_POLICY_MEDIA_PROCESSING); // TODO (b/271950506): revisit it in the next release. // Hide the file management type for now. If anyone uses it, will default to "none". mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SPECIAL_USE, diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index cdb92acc5256..843158c0e9fb 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -71,6 +71,12 @@ public final class AssociationInfo implements Parcelable { * @see CompanionDeviceManager#disassociate(int) */ private final boolean mRevoked; + /** + * Indicates that the association is waiting for its corresponding companion app to be installed + * before it can be added to CDM. This is likely because it was restored onto the device from a + * backup. + */ + private final boolean mPending; private final long mTimeApprovedMs; /** * A long value indicates the last time connected reported by selfManaged devices @@ -88,7 +94,7 @@ public final class AssociationInfo implements Parcelable { @Nullable String tag, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, - boolean notifyOnDeviceNearby, boolean revoked, long timeApprovedMs, + boolean notifyOnDeviceNearby, boolean revoked, boolean pending, long timeApprovedMs, long lastTimeConnectedMs, int systemDataSyncFlags) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); @@ -109,6 +115,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = selfManaged; mNotifyOnDeviceNearby = notifyOnDeviceNearby; mRevoked = revoked; + mPending = pending; mTimeApprovedMs = timeApprovedMs; mLastTimeConnectedMs = lastTimeConnectedMs; mSystemDataSyncFlags = systemDataSyncFlags; @@ -236,6 +243,15 @@ public final class AssociationInfo implements Parcelable { } /** + * @return true if the association is waiting for its corresponding app to be installed + * before it can be added to CDM. + * @hide + */ + public boolean isPending() { + return mPending; + } + + /** * @return the last time self reported disconnected for selfManaged only. * @hide */ @@ -318,6 +334,7 @@ public final class AssociationInfo implements Parcelable { + ", mAssociatedDevice=" + mAssociatedDevice + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby + ", mRevoked=" + mRevoked + + ", mPending=" + mPending + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs) + ", mLastTimeConnectedMs=" + ( mLastTimeConnectedMs == Long.MAX_VALUE @@ -336,6 +353,7 @@ public final class AssociationInfo implements Parcelable { && mSelfManaged == that.mSelfManaged && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby && mRevoked == that.mRevoked + && mPending == that.mPending && mTimeApprovedMs == that.mTimeApprovedMs && mLastTimeConnectedMs == that.mLastTimeConnectedMs && Objects.equals(mPackageName, that.mPackageName) @@ -351,7 +369,7 @@ public final class AssociationInfo implements Parcelable { public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName, mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, - mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags); + mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags); } @Override @@ -372,6 +390,7 @@ public final class AssociationInfo implements Parcelable { dest.writeBoolean(mSelfManaged); dest.writeBoolean(mNotifyOnDeviceNearby); dest.writeBoolean(mRevoked); + dest.writeBoolean(mPending); dest.writeLong(mTimeApprovedMs); dest.writeLong(mLastTimeConnectedMs); dest.writeInt(mSystemDataSyncFlags); @@ -389,6 +408,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = in.readBoolean(); mNotifyOnDeviceNearby = in.readBoolean(); mRevoked = in.readBoolean(); + mPending = in.readBoolean(); mTimeApprovedMs = in.readLong(); mLastTimeConnectedMs = in.readLong(); mSystemDataSyncFlags = in.readInt(); @@ -427,6 +447,7 @@ public final class AssociationInfo implements Parcelable { private boolean mSelfManaged; private boolean mNotifyOnDeviceNearby; private boolean mRevoked; + private boolean mPending; private long mTimeApprovedMs; private long mLastTimeConnectedMs; private int mSystemDataSyncFlags; @@ -453,6 +474,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = info.mSelfManaged; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; mRevoked = info.mRevoked; + mPending = info.mPending; mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; @@ -476,6 +498,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = info.mSelfManaged; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; mRevoked = info.mRevoked; + mPending = info.mPending; mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; @@ -549,6 +572,14 @@ public final class AssociationInfo implements Parcelable { } /** @hide */ + @NonNull + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setPending(boolean pending) { + mPending = pending; + return this; + } + + /** @hide */ @TestApi @NonNull @SuppressLint("MissingGetterMatchingBuilder") @@ -606,6 +637,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged, mNotifyOnDeviceNearby, mRevoked, + mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index ee1d117bf71c..d5eee63fee12 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -8099,7 +8099,7 @@ public class Intent implements Parcelable, Cloneable { int end = data.indexOf('/', 14); if (end < 0) { // All we have is a package name. - intent.mPackage = data.substring(14); + intent.mPackage = Uri.decodeIfNeeded(data.substring(14)); if (!explicitAction) { intent.setAction(ACTION_MAIN); } @@ -8107,21 +8107,22 @@ public class Intent implements Parcelable, Cloneable { } else { // Target the Intent at the given package name always. String authority = null; - intent.mPackage = data.substring(14, end); + intent.mPackage = Uri.decodeIfNeeded(data.substring(14, end)); int newEnd; if ((end+1) < data.length()) { if ((newEnd=data.indexOf('/', end+1)) >= 0) { // Found a scheme, remember it. - scheme = data.substring(end+1, newEnd); + scheme = Uri.decodeIfNeeded(data.substring(end + 1, newEnd)); end = newEnd; if (end < data.length() && (newEnd=data.indexOf('/', end+1)) >= 0) { // Found a authority, remember it. - authority = data.substring(end+1, newEnd); + authority = Uri.decodeIfNeeded( + data.substring(end + 1, newEnd)); end = newEnd; } } else { // All we have is a scheme. - scheme = data.substring(end+1); + scheme = Uri.decodeIfNeeded(data.substring(end + 1)); } } if (scheme == null) { @@ -11762,27 +11763,33 @@ public class Intent implements Parcelable, Cloneable { + this); } uri.append("android-app://"); - uri.append(mPackage); + uri.append(Uri.encode(mPackage)); String scheme = null; if (mData != null) { - scheme = mData.getScheme(); + // All values here must be wrapped with Uri#encodeIfNotEncoded because it is + // possible to exploit the Uri API to return a raw unencoded value, which will + // not deserialize properly and may cause the resulting Intent to be transformed + // to a malicious value. + scheme = Uri.encodeIfNotEncoded(mData.getScheme(), null); if (scheme != null) { uri.append('/'); uri.append(scheme); - String authority = mData.getEncodedAuthority(); + String authority = Uri.encodeIfNotEncoded(mData.getEncodedAuthority(), null); if (authority != null) { uri.append('/'); uri.append(authority); - String path = mData.getEncodedPath(); + + // Multiple path segments are allowed, don't encode the path / separator + String path = Uri.encodeIfNotEncoded(mData.getEncodedPath(), "/"); if (path != null) { uri.append(path); } - String queryParams = mData.getEncodedQuery(); + String queryParams = Uri.encodeIfNotEncoded(mData.getEncodedQuery(), null); if (queryParams != null) { uri.append('?'); uri.append(queryParams); } - String fragment = mData.getEncodedFragment(); + String fragment = Uri.encodeIfNotEncoded(mData.getEncodedFragment(), null); if (fragment != null) { uri.append('#'); uri.append(fragment); diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 30871e938e68..9fe8af516694 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -23,7 +23,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.Activity; -import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; @@ -37,7 +36,6 @@ import android.content.res.TypedArray; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; -import android.os.UserHandle; import android.util.ArraySet; import android.util.Printer; import android.window.OnBackInvokedCallback; @@ -1790,8 +1788,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * @hide */ public boolean isChangeEnabled(long changeId) { - return CompatChanges.isChangeEnabled(changeId, applicationInfo.packageName, - UserHandle.getUserHandleForUid(applicationInfo.uid)); + return applicationInfo.isChangeEnabled(changeId); } /** @hide */ diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 869c621e8564..a8dba5182e90 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -26,6 +26,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; +import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; @@ -381,10 +382,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * start it unless initiated by a user interaction (typically launching its icon * from the launcher, could also include user actions like adding it as an app widget, * selecting it as a live wallpaper, selecting it as a keyboard, etc). Stopped - * applications will not receive broadcasts unless the sender specifies + * applications will not receive implicit broadcasts unless the sender specifies * {@link android.content.Intent#FLAG_INCLUDE_STOPPED_PACKAGES}. * - * <p>Applications should avoid launching activies, binding to or starting services, or + * <p>Applications should avoid launching activities, binding to or starting services, or * otherwise causing a stopped application to run unless initiated by the user. * * <p>An app can also return to the stopped state by a "force stop". @@ -2645,6 +2646,17 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } /** + * Checks if a changeId is enabled for the current user + * @param changeId The changeId to verify + * @return True of the changeId is enabled + * @hide + */ + public boolean isChangeEnabled(long changeId) { + return CompatChanges.isChangeEnabled(changeId, packageName, + UserHandle.getUserHandleForUid(uid)); + } + + /** * @return whether the app has requested exemption from the foreground service restrictions. * This does not take any affect for now. * @hide diff --git a/core/java/android/content/pm/ModuleInfo.java b/core/java/android/content/pm/ModuleInfo.java index a1c874725d4b..c6e93bb302bb 100644 --- a/core/java/android/content/pm/ModuleInfo.java +++ b/core/java/android/content/pm/ModuleInfo.java @@ -16,7 +16,6 @@ package android.content.pm; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; @@ -122,18 +121,15 @@ public final class ModuleInfo implements Parcelable { return mApexModuleName; } - /** @hide Sets the list of the package name of APK-in-APEX apps in this module. */ + /** @hide Set the list of the package names of all APK-in-APEX apps in this module. */ public ModuleInfo setApkInApexPackageNames(@NonNull Collection<String> apkInApexPackageNames) { Objects.requireNonNull(apkInApexPackageNames); mApkInApexPackageNames = List.copyOf(apkInApexPackageNames); return this; } - /** - * Gets the list of the package name of all APK-in-APEX apps in the module. - */ + /** @hide Get the list of the package names of all APK-in-APEX apps in the module. */ @NonNull - @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) public Collection<String> getApkInApexPackageNames() { if (mApkInApexPackageNames == null) { return Collections.emptyList(); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 0e131b413d0c..433226413917 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -672,6 +672,13 @@ public class PackageInstaller { public @interface UserActionReason {} /** + * The unarchival status is not set. + * + * @hide + */ + public static final int UNARCHIVAL_STATUS_UNSET = -1; + + /** * The unarchival is possible and will commence. * * <p> Note that this does not mean that the unarchival has completed. This status should be @@ -736,6 +743,7 @@ public class PackageInstaller { * @hide */ @IntDef(value = { + UNARCHIVAL_STATUS_UNSET, UNARCHIVAL_OK, UNARCHIVAL_ERROR_USER_ACTION_NEEDED, UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE, @@ -2696,8 +2704,6 @@ public class PackageInstaller { public int developmentInstallFlags = 0; /** {@hide} */ public int unarchiveId = -1; - /** {@hide} */ - public IntentSender unarchiveIntentSender; private final ArrayMap<String, Integer> mPermissionStates; @@ -2750,7 +2756,6 @@ public class PackageInstaller { applicationEnabledSettingPersistent = source.readBoolean(); developmentInstallFlags = source.readInt(); unarchiveId = source.readInt(); - unarchiveIntentSender = source.readParcelable(null, IntentSender.class); } /** {@hide} */ @@ -2785,7 +2790,6 @@ public class PackageInstaller { ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; ret.developmentInstallFlags = developmentInstallFlags; ret.unarchiveId = unarchiveId; - ret.unarchiveIntentSender = unarchiveIntentSender; return ret; } @@ -3495,7 +3499,6 @@ public class PackageInstaller { applicationEnabledSettingPersistent); pw.printHexPair("developmentInstallFlags", developmentInstallFlags); pw.printPair("unarchiveId", unarchiveId); - pw.printPair("unarchiveIntentSender", unarchiveIntentSender); pw.println(); } @@ -3540,7 +3543,6 @@ public class PackageInstaller { dest.writeBoolean(applicationEnabledSettingPersistent); dest.writeInt(developmentInstallFlags); dest.writeInt(unarchiveId); - dest.writeParcelable(unarchiveIntentSender, flags); } public static final Parcelable.Creator<SessionParams> diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 0fb0993d0f04..8e5e8250c85d 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -11534,14 +11534,14 @@ public abstract class PackageManager { } /** - * Retrieve AndroidManifest.xml information for the given application apk path. + * Retrieve AndroidManifest.xml information for the given application apk file. * * <p>Example: * * <pre><code> * Bundle result; * try { - * result = getContext().getPackageManager().parseAndroidManifest(apkFilePath, + * result = getContext().getPackageManager().parseAndroidManifest(apkFile, * xmlResourceParser -> { * Bundle bundle = new Bundle(); * // Search the start tag @@ -11570,9 +11570,10 @@ public abstract class PackageManager { * * Note: When the parserFunction is invoked, the client can read the AndroidManifest.xml * information by the XmlResourceParser object. After leaving the parserFunction, the - * XmlResourceParser object will be closed. + * XmlResourceParser object will be closed. The caller should also handle the exception for + * calling this method. * - * @param apkFilePath The path of an application apk file. + * @param apkFile The file of an application apk. * @param parserFunction The parserFunction will be invoked with the XmlResourceParser object * after getting the AndroidManifest.xml of an application package. * @@ -11583,7 +11584,7 @@ public abstract class PackageManager { */ @FlaggedApi(android.content.pm.Flags.FLAG_GET_PACKAGE_INFO) @WorkerThread - public <T> T parseAndroidManifest(@NonNull String apkFilePath, + public <T> T parseAndroidManifest(@NonNull File apkFile, @NonNull Function<XmlResourceParser, T> parserFunction) throws IOException { throw new UnsupportedOperationException( "parseAndroidManifest not implemented in subclass"); diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 4d704c34195f..ae46c027505e 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -17,6 +17,7 @@ package android.content.pm; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.RequiresPermission; import android.os.Parcel; @@ -471,6 +472,17 @@ public class ServiceInfo extends ComponentInfo public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 1 << 12; /** + * Constant corresponding to {@code mediaProcessing} in + * the {@link android.R.attr#foregroundServiceType} attribute. + * Media processing use cases such as video or photo editing and processing. + */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING + ) + @FlaggedApi(Flags.FLAG_INTRODUCE_MEDIA_PROCESSING_TYPE) + public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING = 1 << 13; + + /** * Constant corresponding to {@code specialUse} in * the {@link android.R.attr#foregroundServiceType} attribute. * Use cases that can't be categorized into any other foreground service types, but also @@ -554,6 +566,7 @@ public class ServiceInfo extends ComponentInfo FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT, + FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, FOREGROUND_SERVICE_TYPE_SPECIAL_USE, }) @Retention(RetentionPolicy.SOURCE) @@ -640,6 +653,8 @@ public class ServiceInfo extends ComponentInfo return "shortService"; case FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT: return "fileManagement"; + case FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING: + return "mediaProcessing"; case FOREGROUND_SERVICE_TYPE_SPECIAL_USE: return "specialUse"; default: diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 94bec3562bb0..a2cd3e153b3e 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -131,3 +131,18 @@ flag { bug: "310801107" is_fixed_read_only: true } + +flag { + name: "introduce_media_processing_type" + namespace: "backstage_power" + description: "Add a new FGS type for media processing use cases." + bug: "317788011" +} + +flag { + name: "encode_app_intent" + namespace: "package_manager_service" + description: "Feature flag to encode app intent." + bug: "281848623" +} + diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java index e511469262d1..89f4985461b7 100644 --- a/core/java/android/content/res/Element.java +++ b/core/java/android/content/res/Element.java @@ -26,6 +26,8 @@ import androidx.annotation.StyleableRes; import com.android.internal.R; +import java.util.Set; + /** * Defines the string attribute length and child tag count restrictions for a xml element. * @@ -37,7 +39,11 @@ public class Element { private static final int MAX_ATTR_LEN_URL_COMPONENT = 256; private static final int MAX_ATTR_LEN_PERMISSION_GROUP = 256; private static final int MAX_ATTR_LEN_PACKAGE = 256; - private static final int MAX_ATTR_LEN_MIMETYPE = 512; + /** + * The mime type max length restriction here should match the restriction that is also + * placed in {@link android.content.pm.PackageManager#setMimeGroup(String, Set)} + */ + private static final int MAX_ATTR_LEN_MIMETYPE = 255; private static final int MAX_ATTR_LEN_NAME = 1024; private static final int MAX_ATTR_LEN_PATH = 4000; private static final int MAX_ATTR_LEN_VALUE = 32_768; @@ -103,6 +109,7 @@ public class Element { protected static final String TAG_ATTR_HOST = "host"; protected static final String TAG_ATTR_MANAGE_SPACE_ACTIVITY = "manageSpaceActivity"; protected static final String TAG_ATTR_MIMETYPE = "mimeType"; + protected static final String TAG_ATTR_MIMEGROUP = "mimeGroup"; protected static final String TAG_ATTR_NAME = "name"; protected static final String TAG_ATTR_PACKAGE = "package"; protected static final String TAG_ATTR_PATH = "path"; @@ -367,6 +374,7 @@ public class Element { case TAG_ATTR_BACKUP_AGENT: case TAG_ATTR_CATEGORY: case TAG_ATTR_MANAGE_SPACE_ACTIVITY: + case TAG_ATTR_MIMEGROUP: case TAG_ATTR_NAME: case TAG_ATTR_PARENT_ACTIVITY_NAME: case TAG_ATTR_PERMISSION: @@ -520,6 +528,8 @@ public class Element { return MAX_ATTR_LEN_URL_COMPONENT; case R.styleable.AndroidManifestData_mimeType: return MAX_ATTR_LEN_MIMETYPE; + case R.styleable.AndroidManifestData_mimeGroup: + return MAX_ATTR_LEN_NAME; case R.styleable.AndroidManifestData_path: case R.styleable.AndroidManifestData_pathPattern: case R.styleable.AndroidManifestData_pathPrefix: diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 3fcb3daaa1f2..47ee76e50c9a 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -26,6 +26,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -755,7 +756,10 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, + ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle()); } catch (IntentSender.SendIntentException e) { Log.e( TAG, diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index b96d83247591..ecffe9e5a8b2 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -121,8 +121,12 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) private long mConnectionPtr; + // Restrict this connection to read-only operations. private boolean mOnlyAllowReadOnlyOperations; + // Allow this connection to treat updates to temporary tables as read-only operations. + private boolean mAllowTempTableRetry = Flags.sqliteAllowTempTables(); + // The number of times attachCancellationSignal has been called. // Because SQLite statement execution can be reentrant, we keep track of how many // times we have attempted to attach a cancellation signal to the connection so that @@ -142,6 +146,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private static native void nativeFinalizeStatement(long connectionPtr, long statementPtr); private static native int nativeGetParameterCount(long connectionPtr, long statementPtr); private static native boolean nativeIsReadOnly(long connectionPtr, long statementPtr); + private static native boolean nativeUpdatesTempOnly(long connectionPtr, long statementPtr); private static native int nativeGetColumnCount(long connectionPtr, long statementPtr); private static native String nativeGetColumnName(long connectionPtr, long statementPtr, int index); @@ -1097,7 +1102,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen try { final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr); final int type = DatabaseUtils.getSqlStatementTypeExtended(sql); - final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); + boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly, seqNum); if (!skipCache && isCacheable(type)) { @@ -1265,13 +1270,20 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen /** * Verify that the statement is read-only, if the connection only allows read-only - * operations. + * operations. If the connection allows updates to temporary tables, then the statement is + * read-only if the only updates are to temporary tables. * @param statement The statement to check. * @throws SQLiteException if the statement could update the database inside a read-only * transaction. */ void throwIfStatementForbidden(PreparedStatement statement) { if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) { + if (mAllowTempTableRetry) { + statement.mReadOnly = + nativeUpdatesTempOnly(mConnectionPtr, statement.mStatementPtr); + if (statement.mReadOnly) return; + } + throw new SQLiteException("Cannot execute this statement because it " + "might modify the database but the connection is read-only."); } diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig index 62a51236a2e2..92ef9c24c4ef 100644 --- a/core/java/android/database/sqlite/flags.aconfig +++ b/core/java/android/database/sqlite/flags.aconfig @@ -7,3 +7,11 @@ flag { description: "SQLite APIs held back for Android 15" bug: "279043253" } + +flag { + name: "sqlite_allow_temp_tables" + namespace: "system_performance" + is_fixed_read_only: true + description: "Permit updates to TEMP tables in read-only transactions" + bug: "317993835" +} diff --git a/core/java/android/hardware/SyncFence.java b/core/java/android/hardware/SyncFence.java index d6052cd4c67f..c2440fbaab1b 100644 --- a/core/java/android/hardware/SyncFence.java +++ b/core/java/android/hardware/SyncFence.java @@ -16,6 +16,7 @@ package android.hardware; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.media.Image; import android.media.ImageWriter; @@ -26,6 +27,8 @@ import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.SystemClock; +import com.android.window.flags.Flags; + import libcore.util.NativeAllocationRegistry; import java.io.FileDescriptor; @@ -121,6 +124,19 @@ public final class SyncFence implements AutoCloseable, Parcelable { } } + /** + * Creates a copy of the SyncFence from an existing one. + * Both fences must be closed() independently. + */ + @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) + public SyncFence(@NonNull SyncFence other) { + this(other.mNativePtr); + + if (mNativePtr != 0) { + nIncRef(mNativePtr); + } + } + private SyncFence() { mCloser = () -> {}; } @@ -312,4 +328,5 @@ public final class SyncFence implements AutoCloseable, Parcelable { private static native int nGetFd(long nPtr); private static native boolean nWait(long nPtr, long timeout); private static native long nGetSignalTime(long nPtr); + private static native void nIncRef(long nPtr); } diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index 9e09759a4282..56f69a67c0b5 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -450,11 +450,14 @@ public final class VirtualDisplayConfig implements Parcelable { * automatically launched upon the display creation. If unset or set to {@code false}, the * display will not host any activities upon creation.</p> * - * <p>Note: setting to {@code true} requires the display to be trusted. If the display is - * not trusted, this property is ignored.</p> + * <p>Note: setting to {@code true} requires the display to be trusted and to not mirror + * content of other displays. If the display is not trusted, or if it mirrors content of + * other displays, this property is ignored.</p> * * @param isHomeSupported whether home activities are supported on the display * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED + * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR + * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY * @hide */ @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME) diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index c5167dbc7d4c..a3a2a2e6fd16 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -304,11 +304,7 @@ public final class ProgramList implements AutoCloseable { * * @param id primary identifier of a program to fetch * @return the program info, or null if there is no such program on the list - * - * @deprecated Use {@link #getProgramInfos(ProgramSelector.Identifier)} to get all programs - * with the given primary identifier */ - @Deprecated public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) { Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries; synchronized (mLock) { diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index 7e5c141a399a..4c95e026a180 100644 --- a/core/java/android/hardware/radio/ProgramSelector.java +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -312,20 +312,14 @@ public final class ProgramSelector implements Parcelable { public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; /** * 1: AM, 2:FM - * @deprecated use {@link #IDENTIFIER_TYPE_DRMO_FREQUENCY} instead */ - @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; /** * 32bit primary identifier for SiriusXM Satellite Radio. - * - * @deprecated SiriusXM Satellite Radio is not supported */ public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; /** * 0-999 range - * - * @deprecated SiriusXM Satellite Radio is not supported */ public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; /** diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java index f0f7e8a22e2a..41f21efd60dd 100644 --- a/core/java/android/hardware/radio/RadioManager.java +++ b/core/java/android/hardware/radio/RadioManager.java @@ -166,12 +166,7 @@ public class RadioManager { * analog handover state managed from the HAL implementation side. * * <p>Some radio technologies may not support this, i.e. DAB. - * - * @deprecated Use {@link #CONFIG_FORCE_ANALOG_FM} instead. If {@link #CONFIG_FORCE_ANALOG_FM} - * is supported in HAL, {@link RadioTuner#setConfigFlag} and {@link RadioTuner#isConfigFlagSet} - * with CONFIG_FORCE_ANALOG will set/get the value of {@link #CONFIG_FORCE_ANALOG_FM}. */ - @Deprecated public static final int CONFIG_FORCE_ANALOG = 2; /** * Forces the digital playback for the supporting radio technology. diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 70de477e9e2e..05a3e182135c 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Intent; +import android.content.pm.Flags; import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; @@ -1971,6 +1972,42 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } /** + * Encodes a value it wasn't already encoded. + * + * @param value string to encode + * @param allow characters to allow + * @return encoded value + * @hide + */ + public static String encodeIfNotEncoded(@Nullable String value, @Nullable String allow) { + if (value == null) return null; + if (!Flags.encodeAppIntent() || isEncoded(value, allow)) return value; + return encode(value, allow); + } + + /** + * Returns true if the given string is already encoded to safe characters. + * + * @param value string to check + * @param allow characters to allow + * @return true if the string is already encoded or false if it should be encoded + */ + private static boolean isEncoded(@Nullable String value, @Nullable String allow) { + if (value == null) return true; + for (int index = 0; index < value.length(); index++) { + char c = value.charAt(index); + + // Allow % because that's the prefix for an encoded character. This method will fail + // for decoded strings whose onlyinvalid character is %, but it's assumed that % alone + // cannot cause malicious behavior in the framework. + if (!isAllowed(c, allow) && c != '%') { + return false; + } + } + return true; + } + + /** * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. * Replaces invalid octets with the unicode replacement character * ("\\uFFFD"). @@ -1988,6 +2025,18 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } /** + * Decodes a string if it was encoded, indicated by containing a %. + * @param value encoded string to decode + * @return decoded value + * @hide + */ + public static String decodeIfNeeded(@Nullable String value) { + if (value == null) return null; + if (Flags.encodeAppIntent() && value.contains("%")) return decode(value); + return value; + } + + /** * Support for part implementations. */ static abstract class AbstractPart { diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 5a40e424ea91..75f5491a416f 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -812,8 +812,8 @@ public final class NfcAdapter { if (context == null) { throw new IllegalArgumentException("context cannot be null"); } - context = context.getApplicationContext(); - if (context == null) { + Context applicationContext = context.getApplicationContext(); + if (applicationContext == null) { throw new IllegalArgumentException( "context not associated with any application (using a mock context?)"); } @@ -1802,7 +1802,7 @@ public final class NfcAdapter { * Use {@link #FLAG_LISTEN_DISABLE} to disable listening. * Also refer to {@link #resetDiscoveryTechnology(Activity)} to restore these changes. * </p> - * The pollTech, listenTech parameters can be one or several of below list. + * The pollTechnology, listenTechnology parameters can be one or several of below list. * <pre> * Poll Listen * Passive A 0x01 (NFC_A) 0x01 (NFC_PASSIVE_A) @@ -1820,25 +1820,25 @@ public final class NfcAdapter { * NfcAdapter.FLAG_READER_DISABLE, NfcAdapter.FLAG_LISTEN_KEEP); * }</pre></p> * @param activity The Activity that requests NFC controller to enable specific technologies. - * @param pollTech Flags indicating poll technologies. - * @param listenTech Flags indicating listen technologies. + * @param pollTechnology Flags indicating poll technologies. + * @param listenTechnology Flags indicating listen technologies. * @throws UnsupportedOperationException if FEATURE_NFC, * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF are unavailable. */ @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) public void setDiscoveryTechnology(@NonNull Activity activity, - @PollTechnology int pollTech, @ListenTechnology int listenTech) { - if (listenTech == FLAG_LISTEN_DISABLE) { + @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) { + if (listenTechnology == FLAG_LISTEN_DISABLE) { synchronized (sLock) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } } - mNfcActivityManager.enableReaderMode(activity, null, pollTech, null); + mNfcActivityManager.enableReaderMode(activity, null, pollTechnology, null); return; } - if (pollTech == FLAG_READER_DISABLE) { + if (pollTechnology == FLAG_READER_DISABLE) { synchronized (sLock) { if (!sHasCeFeature) { throw new UnsupportedOperationException(); @@ -1851,7 +1851,7 @@ public final class NfcAdapter { } } } - mNfcActivityManager.setDiscoveryTech(activity, pollTech, listenTech); + mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); } /** diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java index ad86d70db967..81eab71fe080 100644 --- a/core/java/android/nfc/cardemulation/CardEmulation.java +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -69,7 +69,12 @@ public final class CardEmulation { * specified in {@link #EXTRA_CATEGORY}. There is an optional * extra field using {@link Intent#EXTRA_USER} to specify * the {@link UserHandle} of the user that owns the app. + * + * @deprecated Please use {@link android.app.role.RoleManager#createRequestRoleIntent(String)} + * with {@link android.app.role.RoleManager#ROLE_WALLET} parameter + * and {@link Activity#startActivityForResult(Intent, int)} instead. */ + @Deprecated @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; @@ -1084,4 +1089,32 @@ public final class CardEmulation { sService = adapter.getCardEmulationService(); } + /** + * Returns the {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT} for the given user. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED) + @Nullable + public ApduServiceInfo getPreferredPaymentService() { + try { + return sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return null; + } + } + } + } diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java index c5f56144c29c..67e21957839a 100644 --- a/core/java/android/os/AggregateBatteryConsumer.java +++ b/core/java/android/os/AggregateBatteryConsumer.java @@ -33,6 +33,7 @@ import java.io.PrintWriter; * * {@hide} */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class AggregateBatteryConsumer extends BatteryConsumer { static final int CONSUMER_TYPE_AGGREGATE = 0; diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 1a0ce702e38d..b68b94d5bf2d 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -37,6 +37,7 @@ import android.server.ServerProtoEnums; import android.service.batterystats.BatteryStatsServiceDumpHistoryProto; import android.service.batterystats.BatteryStatsServiceDumpProto; import android.telephony.CellSignalStrength; +import android.telephony.ModemActivityInfo; import android.telephony.ServiceState; import android.telephony.TelephonyManager; import android.text.format.DateFormat; @@ -83,6 +84,7 @@ import java.util.Map; * except where indicated otherwise. * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public abstract class BatteryStats { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) @@ -463,6 +465,7 @@ public abstract class BatteryStats { /** * State for keeping track of long counting information. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static abstract class LongCounter { /** @@ -2724,12 +2727,6 @@ public abstract class BatteryStats { */ public abstract int getMobileRadioActiveUnknownCount(int which); - public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0; - public static final int DATA_CONNECTION_EMERGENCY_SERVICE = - TelephonyManager.getAllNetworkTypes().length + 1; - public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1; - - static final String[] DATA_CONNECTION_NAMES = { "oos", "gprs", "edge", "umts", "cdma", "evdo_0", "evdo_A", "1xrtt", "hsdpa", "hsupa", "hspa", "iden", "evdo_b", "lte", @@ -2737,9 +2734,28 @@ public abstract class BatteryStats { "emngcy", "other" }; + public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0; + public static final int DATA_CONNECTION_EMERGENCY_SERVICE = getEmergencyNetworkConnectionType(); + public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1; + @UnsupportedAppUsage public static final int NUM_DATA_CONNECTION_TYPES = DATA_CONNECTION_OTHER + 1; + @android.ravenwood.annotation.RavenwoodReplace + private static int getEmergencyNetworkConnectionType() { + int count = TelephonyManager.getAllNetworkTypes().length; + if (DATA_CONNECTION_NAMES.length != count + 3) { // oos, emngcy, other + throw new IllegalStateException( + "DATA_CONNECTION_NAMES length does not match network type count. " + + "Expected: " + (count + 3) + ", actual:" + DATA_CONNECTION_NAMES.length); + } + return count + 1; + } + + private static int getEmergencyNetworkConnectionType$ravenwood() { + return DATA_CONNECTION_NAMES.length - 2; + } + /** * Returns the time in microseconds that the phone has been running with * the given data connection. @@ -9015,4 +9031,44 @@ public abstract class BatteryStats { (lhs, rhs) -> Double.compare(rhs.millisecondsPerPacket, lhs.millisecondsPerPacket)); return uidMobileRadioStats; } + + @android.ravenwood.annotation.RavenwoodReplace + @VisibleForTesting + protected static boolean isLowRamDevice() { + return ActivityManager.isLowRamDeviceStatic(); + } + + protected static boolean isLowRamDevice$ravenwood() { + return false; + } + + @android.ravenwood.annotation.RavenwoodReplace + @VisibleForTesting + protected static int getCellSignalStrengthLevelCount() { + return CellSignalStrength.getNumSignalStrengthLevels(); + } + + protected static int getCellSignalStrengthLevelCount$ravenwood() { + return 5; + } + + @android.ravenwood.annotation.RavenwoodReplace + @VisibleForTesting + protected static int getModemTxPowerLevelCount() { + return ModemActivityInfo.getNumTxPowerLevels(); + } + + protected static int getModemTxPowerLevelCount$ravenwood() { + return 5; + } + + @android.ravenwood.annotation.RavenwoodReplace + @VisibleForTesting + protected static boolean isKernelStatsAvailable() { + return true; + } + + protected static boolean isKernelStatsAvailable$ravenwood() { + return false; + } } diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index 511f4649e4d4..90d82e7ad2a4 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -56,6 +56,7 @@ import java.util.List; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class BatteryUsageStats implements Parcelable, Closeable { /** diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java index 32840d4b5837..203ef47d857e 100644 --- a/core/java/android/os/BatteryUsageStatsQuery.java +++ b/core/java/android/os/BatteryUsageStatsQuery.java @@ -28,6 +28,7 @@ import java.lang.annotation.RetentionPolicy; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class BatteryUsageStatsQuery implements Parcelable { @NonNull diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index a9b7257a5406..58717179d64d 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -1315,9 +1315,7 @@ public class Build { if (IS_ENG) return true; if (IS_TREBLE_ENABLED) { - // If we can run this code, the device should already pass AVB. - // So, we don't need to check AVB here. - int result = VintfObject.verifyWithoutAvb(); + int result = VintfObject.verifyBuildAtBoot(); if (result != 0) { Slog.e(TAG, "Vendor interface is incompatible, error=" diff --git a/core/java/android/os/ISystemConfig.aidl b/core/java/android/os/ISystemConfig.aidl index 61b24aa55e30..b7649ba9700b 100644 --- a/core/java/android/os/ISystemConfig.aidl +++ b/core/java/android/os/ISystemConfig.aidl @@ -52,4 +52,9 @@ interface ISystemConfig { * @see SystemConfigManager#getDefaultVrComponents */ List<ComponentName> getDefaultVrComponents(); + + /** + * @see SystemConfigManager#getPreventUserDisablePackages + */ + List<String> getPreventUserDisablePackages(); } diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS index d3f2c7ae6e42..eb5b511aa39b 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -94,4 +94,8 @@ per-file CoolingDevice.java = file:/THERMAL_OWNERS per-file Temperature.java = file:/THERMAL_OWNERS # SecurityStateManager -per-file *SecurityStateManager* = file:/SECURITY_STATE_OWNERS
\ No newline at end of file +per-file *SecurityStateManager* = file:/SECURITY_STATE_OWNERS + +# SystemConfig +per-file ISystemConfig.aidl = file:/PACKAGE_MANAGER_OWNERS +per-file SystemConfigManager.java = file:/PACKAGE_MANAGER_OWNERS diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java index e0059105c21f..37bde3db5e14 100644 --- a/core/java/android/os/PerformanceHintManager.java +++ b/core/java/android/os/PerformanceHintManager.java @@ -149,13 +149,47 @@ public final class PerformanceHintManager { @TestApi public static final int CPU_LOAD_RESUME = 3; + /** + * This hint indicates an increase in GPU workload intensity. It means that + * this hint session needs extra GPU resources to meet the target duration. + * This hint must be sent before reporting the actual duration to the session. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION) + public static final int GPU_LOAD_UP = 5; + + /** + * This hint indicates a decrease in GPU workload intensity. It means that + * this hint session can reduce GPU resources and still meet the target duration. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION) + public static final int GPU_LOAD_DOWN = 6; + + /** + * This hint indicates an upcoming GPU workload that is completely changed and + * unknown. It means that the hint session should reset GPU resources to a known + * baseline to prepare for an arbitrary load, and must wake up if inactive. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_ADPF_GPU_REPORT_ACTUAL_WORK_DURATION) + public static final int GPU_LOAD_RESET = 7; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"CPU_LOAD_"}, value = { CPU_LOAD_UP, CPU_LOAD_DOWN, CPU_LOAD_RESET, - CPU_LOAD_RESUME + CPU_LOAD_RESUME, + GPU_LOAD_UP, + GPU_LOAD_DOWN }) public @interface Hint {} diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java index 9c11ad433b8f..164e2659d654 100644 --- a/core/java/android/os/PowerComponents.java +++ b/core/java/android/os/PowerComponents.java @@ -40,6 +40,7 @@ import java.io.PrintWriter; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass class PowerComponents { private final BatteryConsumer.BatteryConsumerData mData; diff --git a/core/java/android/os/SystemConfigManager.java b/core/java/android/os/SystemConfigManager.java index 77843d9fbb0a..21ffbf18dbc3 100644 --- a/core/java/android/os/SystemConfigManager.java +++ b/core/java/android/os/SystemConfigManager.java @@ -161,4 +161,18 @@ public class SystemConfigManager { } return Collections.emptyList(); } + + /** + * Return the packages that are prevented from being disabled, where if + * disabled it would result in a non-functioning system or similar. + * @hide + */ + @NonNull + public List<String> getPreventUserDisablePackages() { + try { + return mInterface.getPreventUserDisablePackages(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java index 3eea94eaf2e6..53af838cb535 100644 --- a/core/java/android/os/UidBatteryConsumer.java +++ b/core/java/android/os/UidBatteryConsumer.java @@ -37,6 +37,7 @@ import java.lang.annotation.RetentionPolicy; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class UidBatteryConsumer extends BatteryConsumer { static final int CONSUMER_TYPE_UID = 1; @@ -223,6 +224,7 @@ public final class UidBatteryConsumer extends BatteryConsumer { /** * Builder for UidBatteryConsumer. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class Builder extends BaseBuilder<Builder> { private static final String PACKAGE_NAME_UNINITIALIZED = ""; private final BatteryStats.Uid mBatteryStatsUid; diff --git a/core/java/android/os/VintfObject.java b/core/java/android/os/VintfObject.java index 1f11197afeee..4fc5131617b2 100644 --- a/core/java/android/os/VintfObject.java +++ b/core/java/android/os/VintfObject.java @@ -18,7 +18,6 @@ package android.os; import android.annotation.NonNull; import android.annotation.TestApi; -import android.util.Slog; import java.util.Map; @@ -44,44 +43,8 @@ public class VintfObject { public static native String[] report(); /** - * Verify that the given metadata for an OTA package is compatible with - * this device. - * - * @param packageInfo a list of serialized form of HalManifest's / - * CompatibilityMatri'ces (XML). - * @return = 0 if success (compatible) - * > 0 if incompatible - * < 0 if any error (mount partition fails, illformed XML, etc.) - * - * @deprecated Checking compatibility against an OTA package is no longer - * supported because the format of VINTF metadata in the OTA package may not - * be recognized by the current system. - * - * <p> - * <ul> - * <li>This function always returns 0 for non-empty {@code packageInfo}. - * </li> - * <li>This function returns the result of {@link #verifyWithoutAvb} for - * null or empty {@code packageInfo}.</li> - * </ul> - * - * @hide - */ - @Deprecated - public static int verify(String[] packageInfo) { - if (packageInfo != null && packageInfo.length > 0) { - Slog.w(LOG_TAG, "VintfObject.verify() with non-empty packageInfo is deprecated. " - + "Skipping compatibility checks for update package."); - return 0; - } - Slog.w(LOG_TAG, "VintfObject.verify() is deprecated. Call verifyWithoutAvb() instead."); - return verifyWithoutAvb(); - } - - /** - * Verify Vintf compatibility on the device without checking AVB - * (Android Verified Boot). It is useful to verify a running system - * image where AVB check is irrelevant. + * Verify Vintf compatibility on the device at boot time. Certain checks + * like kernel checks, AVB checks are disabled. * * @return = 0 if success (compatible) * > 0 if incompatible @@ -89,7 +52,7 @@ public class VintfObject { * * @hide */ - public static native int verifyWithoutAvb(); + public static native int verifyBuildAtBoot(); /** * @return a list of HAL names and versions that is supported by this diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index c14810bbcd64..f3496e7f2592 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -425,6 +425,8 @@ public class ZygoteProcess { throw new ZygoteStartFailedEx("Embedded newlines not allowed"); } else if (arg.indexOf('\r') >= 0) { throw new ZygoteStartFailedEx("Embedded carriage returns not allowed"); + } else if (arg.indexOf('\u0000') >= 0) { + throw new ZygoteStartFailedEx("Embedded nulls not allowed"); } } @@ -965,6 +967,14 @@ public class ZygoteProcess { return true; } + for (/* NonNull */ String s : mApiDenylistExemptions) { + // indexOf() is intrinsified and faster than contains(). + if (s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('\u0000') >= 0) { + Slog.e(LOG_TAG, "Failed to set API denylist exemptions: Bad character"); + mApiDenylistExemptions = Collections.emptyList(); + return false; + } + } try { state.mZygoteOutputWriter.write(Integer.toString(mApiDenylistExemptions.size() + 1)); state.mZygoteOutputWriter.newLine(); diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl index 3ecf74e75367..54ed73c34830 100644 --- a/core/java/android/os/storage/IStorageManager.aidl +++ b/core/java/android/os/storage/IStorageManager.aidl @@ -134,16 +134,16 @@ interface IStorageManager { @EnforcePermission("MOUNT_UNMOUNT_FILESYSTEMS") void setDebugFlags(int flags, int mask) = 60; @EnforcePermission("STORAGE_INTERNAL") - void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) = 61; + void createUserStorageKeys(int userId, boolean ephemeral) = 61; @EnforcePermission("STORAGE_INTERNAL") void destroyUserStorageKeys(int userId) = 62; @EnforcePermission("STORAGE_INTERNAL") - void unlockCeStorage(int userId, int serialNumber, in byte[] secret) = 63; + void unlockCeStorage(int userId, in byte[] secret) = 63; @EnforcePermission("STORAGE_INTERNAL") void lockCeStorage(int userId) = 64; boolean isCeStorageUnlocked(int userId) = 65; @EnforcePermission("STORAGE_INTERNAL") - void prepareUserStorage(in String volumeUuid, int userId, int serialNumber, int flags) = 66; + void prepareUserStorage(in String volumeUuid, int userId, int flags) = 66; @EnforcePermission("STORAGE_INTERNAL") void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67; @EnforcePermission("STORAGE_INTERNAL") diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 78a12f75a508..9587db13ea87 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1602,14 +1602,13 @@ public class StorageManager { * This is only intended to be called by UserManagerService, as part of creating a user. * * @param userId ID of the user - * @param serialNumber serial number of the user * @param ephemeral whether the user is ephemeral * @throws RuntimeException on error. The user's keys already existing is considered an error. * @hide */ - public void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) { + public void createUserStorageKeys(int userId, boolean ephemeral) { try { - mStorageManager.createUserStorageKeys(userId, serialNumber, ephemeral); + mStorageManager.createUserStorageKeys(userId, ephemeral); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1653,9 +1652,9 @@ public class StorageManager { } /** {@hide} */ - public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) { + public void prepareUserStorage(String volumeUuid, int userId, int flags) { try { - mStorageManager.prepareUserStorage(volumeUuid, userId, serialNumber, flags); + mStorageManager.prepareUserStorage(volumeUuid, userId, flags); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index fae7cb394906..60143cc79d2d 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -71,3 +71,10 @@ flag { description: "default retail demo role holder" bug: "274132354" } + +flag { + name: "wallet_role_enabled" + namespace: "wallet_integration" + description: "This flag is used to enabled the Wallet Role for all users on the device" + bug: "283989236" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 393214209025..f5260ef34986 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -108,9 +108,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -3584,12 +3586,10 @@ public final class Settings { || applicationInfo.isSignedWithPlatformKey(); } - private ArrayMap<String, String> getStringsForPrefixStripPrefix( - ContentResolver cr, String prefix, String[] names) { + public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix, + List<String> names) { String namespace = prefix.substring(0, prefix.length() - 1); ArrayMap<String, String> keyValues = new ArrayMap<>(); - int substringLength = prefix.length(); - int currentGeneration = -1; boolean needsGenerationTracker = false; @@ -3602,24 +3602,27 @@ public final class Settings { + " type:" + mUri.getPath() + " in package:" + cr.getPackageName()); } + // When a generation number changes, remove cached values, remove the old + // generation tracker and request a new one + generationTracker.destroy(); + mGenerationTrackers.remove(prefix); for (int i = mValues.size() - 1; i >= 0; i--) { String key = mValues.keyAt(i); if (key.startsWith(prefix)) { mValues.remove(key); } } + needsGenerationTracker = true; } else { boolean prefixCached = mValues.containsKey(prefix); if (prefixCached) { if (DEBUG) { Log.i(TAG, "Cache hit for prefix:" + prefix); } - if (names.length > 0) { + if (!names.isEmpty()) { for (String name : names) { if (mValues.containsKey(name)) { - keyValues.put( - name.substring(substringLength), - mValues.get(name)); + keyValues.put(name, mValues.get(name)); } } } else { @@ -3628,9 +3631,7 @@ public final class Settings { // Explicitly exclude the prefix as it is only there to // signal that the prefix has been cached. if (key.startsWith(prefix) && !key.equals(prefix)) { - keyValues.put( - key.substring(substringLength), - mValues.get(key)); + keyValues.put(key, mValues.get(key)); } } } @@ -3690,22 +3691,14 @@ public final class Settings { Map<String, String> flagsToValues = (HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class); // Only the flags requested by the caller - if (names.length > 0) { - for (String name : names) { - String value = flagsToValues.get(name); - if (value != null) { - keyValues.put( - name.substring(substringLength), - value); + if (!names.isEmpty()) { + for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { + if (names.contains(flag.getKey())) { + keyValues.put(flag.getKey(), flag.getValue()); } } } else { - keyValues.ensureCapacity(keyValues.size() + flagsToValues.size()); - for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { - keyValues.put( - flag.getKey().substring(substringLength), - flag.getValue()); - } + keyValues.putAll(flagsToValues); } synchronized (NameValueCache.this) { @@ -10150,8 +10143,12 @@ public final class Settings { /** * The default NFC payment component + * + * @deprecated please use {@link android.app.role.RoleManager#getRoleHolders(String)} + * with {@link android.app.role.RoleManager#ROLE_WALLET} parameter. * @hide */ + @Deprecated @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component"; @@ -12185,6 +12182,7 @@ public final class Settings { CLONE_TO_MANAGED_PROFILE.add(SHOW_IME_WITH_HARD_KEYBOARD); CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_BOUNCE_KEYS); CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_BUBBLES); + CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_HISTORY_ENABLED); } /** @hide */ @@ -19687,6 +19685,15 @@ public final class Settings { @Readable public static final String WRIST_DETECTION_AUTO_LOCKING_ENABLED = "wear_wrist_detection_auto_locking_enabled"; + + /** + * Whether consistent notification blocking experience is enabled. + * + * @hide + */ + @Readable + public static final String CONSISTENT_NOTIFICATION_BLOCKING_ENABLED = + "consistent_notification_blocking_enabled"; } } @@ -19847,15 +19854,21 @@ public final class Settings { @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public static Map<String, String> getStrings(@NonNull ContentResolver resolver, @NonNull String namespace, @NonNull List<String> names) { - String[] compositeNames = new String[names.size()]; - for (int i = 0, size = names.size(); i < size; ++i) { - compositeNames[i] = createCompositeName(namespace, names.get(i)); + List<String> compositeNames = new ArrayList<>(names.size()); + for (String name : names) { + compositeNames.add(createCompositeName(namespace, name)); } String prefix = createPrefix(namespace); - - ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix( + ArrayMap<String, String> rawKeyValues = sNameValueCache.getStringsForPrefix( resolver, prefix, compositeNames); + int size = rawKeyValues.size(); + int substringLength = prefix.length(); + ArrayMap<String, String> keyValues = new ArrayMap<>(size); + for (int i = 0; i < size; ++i) { + keyValues.put(rawKeyValues.keyAt(i).substring(substringLength), + rawKeyValues.valueAt(i)); + } return keyValues; } diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java index 0e82b6c2c7d7..03ebae5c5199 100644 --- a/core/java/android/service/notification/ZenDeviceEffects.java +++ b/core/java/android/service/notification/ZenDeviceEffects.java @@ -17,12 +17,16 @@ package android.service.notification; 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; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Objects; @@ -33,6 +37,76 @@ 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()}. + * @hide + */ + @IntDef(flag = true, prefix = { "FIELD_" }, value = { + FIELD_GRAYSCALE, + FIELD_SUPPRESS_AMBIENT_DISPLAY, + FIELD_DIM_WALLPAPER, + FIELD_NIGHT_MODE, + FIELD_DISABLE_AUTO_BRIGHTNESS, + FIELD_DISABLE_TAP_TO_WAKE, + FIELD_DISABLE_TILT_TO_WAKE, + FIELD_DISABLE_TOUCH, + FIELD_MINIMIZE_RADIO_USAGE, + FIELD_MAXIMIZE_DOZE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ModifiableField {} + + /** + * @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; private final boolean mSuppressAmbientDisplay; private final boolean mDimWallpaper; @@ -45,10 +119,13 @@ 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) { + boolean minimizeRadioUsage, boolean maximizeDoze, + @ModifiableField int userModifiedFields) { mGrayscale = grayscale; mSuppressAmbientDisplay = suppressAmbientDisplay; mDimWallpaper = dimWallpaper; @@ -59,6 +136,7 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = disableTouch; mMinimizeRadioUsage = minimizeRadioUsage; mMaximizeDoze = maximizeDoze; + mUserModifiedFields = userModifiedFields; } @Override @@ -75,14 +153,15 @@ public final class ZenDeviceEffects implements Parcelable { && this.mDisableTiltToWake == that.mDisableTiltToWake && this.mDisableTouch == that.mDisableTouch && this.mMinimizeRadioUsage == that.mMinimizeRadioUsage - && this.mMaximizeDoze == that.mMaximizeDoze; + && this.mMaximizeDoze == that.mMaximizeDoze + && this.mUserModifiedFields == that.mUserModifiedFields; } @Override public int hashCode() { return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, - mMinimizeRadioUsage, mMaximizeDoze); + mMinimizeRadioUsage, mMaximizeDoze, mUserModifiedFields); } @Override @@ -98,7 +177,43 @@ 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) + "]"; + return "[" + String.join(", ", effects) + "]" + + " userModifiedFields: " + modifiedFieldsToString(mUserModifiedFields); + } + + private String modifiedFieldsToString(int bitmask) { + ArrayList<String> modified = new ArrayList<>(); + if ((bitmask & FIELD_GRAYSCALE) != 0) { + modified.add("FIELD_GRAYSCALE"); + } + if ((bitmask & FIELD_SUPPRESS_AMBIENT_DISPLAY) != 0) { + modified.add("FIELD_SUPPRESS_AMBIENT_DISPLAY"); + } + if ((bitmask & FIELD_DIM_WALLPAPER) != 0) { + modified.add("FIELD_DIM_WALLPAPER"); + } + if ((bitmask & FIELD_NIGHT_MODE) != 0) { + modified.add("FIELD_NIGHT_MODE"); + } + if ((bitmask & FIELD_DISABLE_AUTO_BRIGHTNESS) != 0) { + modified.add("FIELD_DISABLE_AUTO_BRIGHTNESS"); + } + if ((bitmask & FIELD_DISABLE_TAP_TO_WAKE) != 0) { + modified.add("FIELD_DISABLE_TAP_TO_WAKE"); + } + if ((bitmask & FIELD_DISABLE_TILT_TO_WAKE) != 0) { + modified.add("FIELD_DISABLE_TILT_TO_WAKE"); + } + if ((bitmask & FIELD_DISABLE_TOUCH) != 0) { + modified.add("FIELD_DISABLE_TOUCH"); + } + if ((bitmask & FIELD_MINIMIZE_RADIO_USAGE) != 0) { + modified.add("FIELD_MINIMIZE_RADIO_USAGE"); + } + if ((bitmask & FIELD_MAXIMIZE_DOZE) != 0) { + modified.add("FIELD_MAXIMIZE_DOZE"); + } + return "{" + String.join(",", modified) + "}"; } /** @@ -194,9 +309,10 @@ public final class ZenDeviceEffects implements Parcelable { public static final Creator<ZenDeviceEffects> CREATOR = new Creator<ZenDeviceEffects>() { @Override public ZenDeviceEffects createFromParcel(Parcel in) { - return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(), + return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), - in.readBoolean(), in.readBoolean(), in.readBoolean()); + in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), + in.readBoolean(), in.readInt()); } @Override @@ -205,6 +321,16 @@ 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; @@ -222,6 +348,7 @@ public final class ZenDeviceEffects implements Parcelable { dest.writeBoolean(mDisableTouch); dest.writeBoolean(mMinimizeRadioUsage); dest.writeBoolean(mMaximizeDoze); + dest.writeInt(mUserModifiedFields); } /** Builder class for {@link ZenDeviceEffects} objects. */ @@ -238,6 +365,7 @@ 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). @@ -260,6 +388,7 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = zenDeviceEffects.shouldDisableTouch(); mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage(); mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze(); + mUserModifiedFields = zenDeviceEffects.mUserModifiedFields; } /** @@ -381,12 +510,24 @@ 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); + return new ZenDeviceEffects(mGrayscale, + mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, + mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage, + mMaximizeDoze, mUserModifiedFields); } } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index fcdc5fe71e4e..45a0c205a09b 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -205,6 +205,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 = "channels"; + private static final String 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"; @@ -247,6 +248,7 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_MODIFIED = "modified"; private static final String RULE_ATT_ALLOW_MANUAL = "userInvokable"; private static final String RULE_ATT_TYPE = "type"; + private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields"; private static final String RULE_ATT_ICON = "rule_icon"; private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc"; @@ -261,6 +263,7 @@ public class ZenModeConfig implements Parcelable { private static final String DEVICE_EFFECT_DISABLE_TOUCH = "zdeDisableTouch"; private static final String DEVICE_EFFECT_MINIMIZE_RADIO_USAGE = "zdeMinimizeRadioUsage"; private static final String DEVICE_EFFECT_MAXIMIZE_DOZE = "zdeMaximizeDoze"; + private static final String DEVICE_EFFECT_USER_MODIFIED_FIELDS = "zdeUserModifiedFields"; @UnsupportedAppUsage public boolean allowAlarms = DEFAULT_ALLOW_ALARMS; @@ -748,6 +751,7 @@ public class ZenModeConfig implements Parcelable { rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON); 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); } return rt; } @@ -794,6 +798,7 @@ public class ZenModeConfig implements Parcelable { out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription); } out.attributeInt(null, RULE_ATT_TYPE, rule.type); + out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields); } } @@ -856,6 +861,7 @@ public class ZenModeConfig implements Parcelable { builder.allowChannels(channels); policySet = true; } + builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0)); } if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) { @@ -968,6 +974,7 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesApi()) { writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getAllowedChannels(), out); + out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields()); } } @@ -993,6 +1000,7 @@ public class ZenModeConfig implements Parcelable { } } + @FlaggedApi(Flags.FLAG_MODES_API) @Nullable private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) { ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() @@ -1012,11 +1020,13 @@ 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; } + @FlaggedApi(Flags.FLAG_MODES_API) private static void writeZenDeviceEffectsXml(ZenDeviceEffects deviceEffects, TypedXmlSerializer out) throws IOException { writeBooleanIfTrue(out, DEVICE_EFFECT_DISPLAY_GRAYSCALE, @@ -1035,6 +1045,8 @@ 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) @@ -1985,6 +1997,7 @@ public class ZenModeConfig implements Parcelable { public String triggerDescription; public String iconResName; public boolean allowManualInvocation; + public int userModifiedFields; public ZenRule() { } @@ -2017,9 +2030,22 @@ public class ZenModeConfig implements Parcelable { iconResName = source.readString(); triggerDescription = source.readString(); type = source.readInt(); + userModifiedFields = source.readInt(); } } + /** + * @see AutomaticZenRule#canUpdate() + */ + @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); + } + @Override public int describeContents() { return 0; @@ -2064,6 +2090,7 @@ public class ZenModeConfig implements Parcelable { dest.writeString(iconResName); dest.writeString(triggerDescription); dest.writeInt(type); + dest.writeInt(userModifiedFields); } } @@ -2092,7 +2119,8 @@ public class ZenModeConfig implements Parcelable { .append(",allowManualInvocation=").append(allowManualInvocation) .append(",iconResName=").append(iconResName) .append(",triggerDescription=").append(triggerDescription) - .append(",type=").append(type); + .append(",type=").append(type) + .append(",userModifiedFields=").append(userModifiedFields); } return sb.append(']').toString(); @@ -2151,7 +2179,8 @@ public class ZenModeConfig implements Parcelable { && other.allowManualInvocation == allowManualInvocation && Objects.equals(other.iconResName, iconResName) && Objects.equals(other.triggerDescription, triggerDescription) - && other.type == type; + && other.type == type + && other.userModifiedFields == userModifiedFields; } return finalEquals; @@ -2163,7 +2192,7 @@ 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); + triggerDescription, type, userModifiedFields); } return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, modified); diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index d87e75884802..8902368072bf 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -467,6 +467,7 @@ public class ZenModeDiff { public static final String FIELD_ICON_RES = "iconResName"; public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription"; public static final String FIELD_TYPE = "type"; + public static final String FIELD_USER_MODIFIED_FIELDS = "userModifiedFields"; // NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule // Special field to track whether this rule became active or inactive @@ -562,6 +563,10 @@ public class ZenModeDiff { if (!Objects.equals(from.iconResName, to.iconResName)) { addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName)); } + if (from.userModifiedFields != to.userModifiedFields) { + addField(FIELD_USER_MODIFIED_FIELDS, + new FieldDiff<>(from.userModifiedFields, to.userModifiedFields)); + } } } diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index 3c1a2796de95..8477eb7120c2 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -20,6 +20,8 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.TestApi; import android.app.Flags; import android.app.Notification; import android.app.NotificationChannel; @@ -32,6 +34,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -41,12 +44,148 @@ import java.util.Objects; * a device is in Do Not Disturb mode. */ public final class ZenPolicy implements Parcelable { - private ArrayList<Integer> mPriorityCategories; - private ArrayList<Integer> mVisualEffects; + + /** Used to track which rule variables have been modified by the user. + * Should be checked against the bitmask {@link #getUserModifiedFields()}. + * @hide + */ + @IntDef(flag = true, prefix = { "FIELD_" }, value = { + FIELD_MESSAGES, + FIELD_CALLS, + FIELD_CONVERSATIONS, + FIELD_ALLOW_CHANNELS, + FIELD_PRIORITY_CATEGORY_REMINDERS, + FIELD_PRIORITY_CATEGORY_EVENTS, + FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS, + FIELD_PRIORITY_CATEGORY_ALARMS, + FIELD_PRIORITY_CATEGORY_MEDIA, + FIELD_PRIORITY_CATEGORY_SYSTEM, + FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT, + FIELD_VISUAL_EFFECT_LIGHTS, + FIELD_VISUAL_EFFECT_PEEK, + FIELD_VISUAL_EFFECT_STATUS_BAR, + FIELD_VISUAL_EFFECT_BADGE, + FIELD_VISUAL_EFFECT_AMBIENT, + FIELD_VISUAL_EFFECT_NOTIFICATION_LIST, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ModifiableField {} + + /** + * Covers modifications to MESSAGE_SENDERS and PRIORITY_CATEGORY_MESSAGES, which are set at + * the same time. + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public static final int FIELD_MESSAGES = 1 << 0; + /** + * Covers modifications to CALL_SENDERS and PRIORITY_CATEGORY_CALLS, which are set at + * the same time. + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public static final int FIELD_CALLS = 1 << 1; + /** + * Covers modifications to CONVERSATION_SENDERS and PRIORITY_CATEGORY_CONVERSATIONS, which are + * 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; + /** + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + public static final int FIELD_PRIORITY_CATEGORY_REMINDERS = 1 << 4; + /** + * @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; + + private List<Integer> mPriorityCategories; + private List<Integer> mVisualEffects; private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET; private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET; private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET; private @ChannelType int mAllowChannels = CHANNEL_TYPE_UNSET; + private final @ModifiableField int mUserModifiedFields; // Bitwise representation /** @hide */ @IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = { @@ -249,6 +388,22 @@ 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) { + mPriorityCategories = priorityCategories; + mVisualEffects = visualEffects; + mPriorityMessages = priorityMessages; + mPriorityCalls = priorityCalls; + mConversationSenders = conversationSenders; + mAllowChannels = allowChannels; + mUserModifiedFields = userModifiedFields; } /** @@ -473,6 +628,8 @@ 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() { @@ -482,9 +639,14 @@ public final class ZenPolicy implements Parcelable { /** * @hide */ - public Builder(ZenPolicy policy) { + @SuppressLint("UnflaggedApi") + @TestApi + public Builder(@Nullable ZenPolicy policy) { if (policy != null) { mZenPolicy = policy.copy(); + if (Flags.modesApi()) { + mUserModifiedFields = policy.mUserModifiedFields; + } } else { mZenPolicy = new ZenPolicy(); } @@ -494,7 +656,15 @@ public final class ZenPolicy implements Parcelable { * Builds the current ZenPolicy. */ public @NonNull ZenPolicy build() { - return mZenPolicy.copy(); + if (Flags.modesApi()) { + return new ZenPolicy(new ArrayList<Integer>(mZenPolicy.mPriorityCategories), + new ArrayList<Integer>(mZenPolicy.mVisualEffects), + mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls, + mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels, + mUserModifiedFields); + } else { + return mZenPolicy.copy(); + } } /** @@ -850,6 +1020,28 @@ public final class ZenPolicy implements Parcelable { mZenPolicy.mAllowChannels = channelType; 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 @@ -861,39 +1053,49 @@ public final class ZenPolicy implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeList(mPriorityCategories); dest.writeList(mVisualEffects); - dest.writeInt(mPriorityCalls); dest.writeInt(mPriorityMessages); + dest.writeInt(mPriorityCalls); dest.writeInt(mConversationSenders); if (Flags.modesApi()) { dest.writeInt(mAllowChannels); + dest.writeInt(mUserModifiedFields); } } - public static final @android.annotation.NonNull Parcelable.Creator<ZenPolicy> CREATOR = - new Parcelable.Creator<ZenPolicy>() { - @Override - public ZenPolicy createFromParcel(Parcel source) { - ZenPolicy policy = new ZenPolicy(); - policy.mPriorityCategories = trimList( - source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class), - NUM_PRIORITY_CATEGORIES); - policy.mVisualEffects = trimList( - source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class), - NUM_VISUAL_EFFECTS); - policy.mPriorityCalls = source.readInt(); - policy.mPriorityMessages = source.readInt(); - policy.mConversationSenders = source.readInt(); - if (Flags.modesApi()) { - policy.mAllowChannels = source.readInt(); - } - return policy; - } + public static final @NonNull Creator<ZenPolicy> CREATOR = + new Creator<ZenPolicy>() { + @Override + public ZenPolicy createFromParcel(Parcel source) { + ZenPolicy policy; + if (Flags.modesApi()) { + policy = new ZenPolicy( + trimList(source.readArrayList(Integer.class.getClassLoader(), + Integer.class), NUM_PRIORITY_CATEGORIES), + trimList(source.readArrayList(Integer.class.getClassLoader(), + Integer.class), NUM_VISUAL_EFFECTS), + source.readInt(), source.readInt(), source.readInt(), + source.readInt(), source.readInt() + ); + } else { + policy = new ZenPolicy(); + policy.mPriorityCategories = + trimList(source.readArrayList(Integer.class.getClassLoader(), + Integer.class), NUM_PRIORITY_CATEGORIES); + policy.mVisualEffects = + trimList(source.readArrayList(Integer.class.getClassLoader(), + Integer.class), NUM_VISUAL_EFFECTS); + policy.mPriorityMessages = source.readInt(); + policy.mPriorityCalls = source.readInt(); + policy.mConversationSenders = source.readInt(); + } + return policy; + } - @Override - public ZenPolicy[] newArray(int size) { - return new ZenPolicy[size]; - } - }; + @Override + public ZenPolicy[] newArray(int size) { + return new ZenPolicy[size]; + } + }; @Override public String toString() { @@ -907,10 +1109,69 @@ 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) { + ArrayList<String> modified = new ArrayList<>(); + if ((bitmask & FIELD_MESSAGES) != 0) { + modified.add("FIELD_MESSAGES"); + } + if ((bitmask & FIELD_CALLS) != 0) { + modified.add("FIELD_CALLS"); + } + if ((bitmask & FIELD_CONVERSATIONS) != 0) { + modified.add("FIELD_CONVERSATIONS"); + } + if ((bitmask & FIELD_ALLOW_CHANNELS) != 0) { + modified.add("FIELD_ALLOW_CHANNELS"); + } + if ((bitmask & FIELD_PRIORITY_CATEGORY_REMINDERS) != 0) { + modified.add("FIELD_PRIORITY_CATEGORY_REMINDERS"); + } + if ((bitmask & FIELD_PRIORITY_CATEGORY_EVENTS) != 0) { + modified.add("FIELD_PRIORITY_CATEGORY_EVENTS"); + } + if ((bitmask & FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS) != 0) { + modified.add("FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS"); + } + if ((bitmask & FIELD_PRIORITY_CATEGORY_ALARMS) != 0) { + modified.add("FIELD_PRIORITY_CATEGORY_ALARMS"); + } + if ((bitmask & FIELD_PRIORITY_CATEGORY_MEDIA) != 0) { + modified.add("FIELD_PRIORITY_CATEGORY_MEDIA"); + } + if ((bitmask & FIELD_PRIORITY_CATEGORY_SYSTEM) != 0) { + modified.add("FIELD_PRIORITY_CATEGORY_SYSTEM"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT) != 0) { + modified.add("FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_LIGHTS) != 0) { + modified.add("FIELD_VISUAL_EFFECT_LIGHTS"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_PEEK) != 0) { + modified.add("FIELD_VISUAL_EFFECT_PEEK"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_STATUS_BAR) != 0) { + modified.add("FIELD_VISUAL_EFFECT_STATUS_BAR"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_BADGE) != 0) { + modified.add("FIELD_VISUAL_EFFECT_BADGE"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_AMBIENT) != 0) { + modified.add("FIELD_VISUAL_EFFECT_AMBIENT"); + } + if ((bitmask & FIELD_VISUAL_EFFECT_NOTIFICATION_LIST) != 0) { + modified.add("FIELD_VISUAL_EFFECT_NOTIFICATION_LIST"); + } + return "{" + String.join(",", modified) + "}"; + } + // Returns a list containing the first maxLength elements of the input list if the list is // longer than that size. For the lists in ZenPolicy, this should not happen unless the input // is corrupt. @@ -1066,7 +1327,8 @@ public final class ZenPolicy implements Parcelable { && other.mPriorityMessages == mPriorityMessages && other.mConversationSenders == mConversationSenders; if (Flags.modesApi()) { - return eq && other.mAllowChannels == mAllowChannels; + return eq && other.mAllowChannels == mAllowChannels + && other.mUserModifiedFields == mUserModifiedFields; } return eq; } @@ -1075,13 +1337,13 @@ public final class ZenPolicy implements Parcelable { public int hashCode() { if (Flags.modesApi()) { return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, - mPriorityMessages, mConversationSenders, mAllowChannels); + mPriorityMessages, mConversationSenders, mAllowChannels, mUserModifiedFields); } return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages, mConversationSenders); } - private @ZenPolicy.State int getZenPolicyPriorityCategoryState(@PriorityCategory int + private @State int getZenPolicyPriorityCategoryState(@PriorityCategory int category) { switch (category) { case PRIORITY_CATEGORY_REMINDERS: @@ -1106,7 +1368,7 @@ public final class ZenPolicy implements Parcelable { return -1; } - private @ZenPolicy.State int getZenPolicyVisualEffectState(@VisualEffect int effect) { + private @State int getZenPolicyVisualEffectState(@VisualEffect int effect) { switch (effect) { case VISUAL_EFFECT_FULL_SCREEN_INTENT: return getVisualEffectFullScreenIntent(); diff --git a/core/java/android/view/DisplayAddress.java b/core/java/android/view/DisplayAddress.java index 99e811a72605..c3c4ab52d5ef 100644 --- a/core/java/android/view/DisplayAddress.java +++ b/core/java/android/view/DisplayAddress.java @@ -138,6 +138,30 @@ public abstract class DisplayAddress implements Parcelable { out.writeLong(mPhysicalDisplayId); } + /** + * This method is meant to check to see if the ports match + * @param a1 Address to compare + * @param a2 Address to compare + * + * @return true if the arguments have the same port, and at least one does not specify + * a model. + */ + public static boolean isPortMatch(DisplayAddress a1, DisplayAddress a2) { + // Both displays must be of type Physical + if (!(a1 instanceof Physical && a2 instanceof Physical)) { + return false; + } + Physical p1 = (Physical) a1; + Physical p2 = (Physical) a2; + + // If both addresses specify a model, fallback to a basic match check (which + // also checks the port). + if (p1.getModel() != null && p2.getModel() != null) { + return p1.equals(p2); + } + return p1.getPort() == p2.getPort(); + } + private Physical(long physicalDisplayId) { mPhysicalDisplayId = physicalDisplayId; } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index b957b31a5bb7..674f22c4166e 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -28,6 +28,7 @@ import static android.view.SurfaceControlProto.NAME; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; @@ -71,6 +72,7 @@ import android.view.Surface.OutOfResourcesException; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import com.android.internal.util.VirtualRefBasePtr; +import com.android.window.flags.Flags; import dalvik.system.CloseGuard; @@ -277,6 +279,8 @@ public final class SurfaceControl implements Parcelable { private static native int nativeGetLayerId(long nativeObject); private static native void nativeAddTransactionCommittedListener(long nativeObject, TransactionCommittedListener listener); + private static native void nativeAddTransactionCompletedListener(long nativeObject, + Consumer<TransactionStats> listener); private static native void nativeSanitize(long transactionObject, int pid, int uid); private static native void nativeSetDestinationFrame(long transactionObj, long nativeObject, int l, int t, int r, int b); @@ -290,6 +294,10 @@ 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 nativeSetFrameTimeline(long transactionObj, + long vsyncId); /** * Transforms that can be applied to buffers as they are displayed to a window. @@ -2550,6 +2558,50 @@ public final class SurfaceControl implements Parcelable { } /** + * Transaction stats given to the listener registered in + * {@link SurfaceControl.Transaction#addTransactionCompletedListener} + */ + @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) + public static final class TransactionStats { + private long mLatchTime; + private SyncFence mSyncFence; + + // called from native + private TransactionStats(long latchTime, long presentFencePtr) { + mLatchTime = latchTime; + mSyncFence = new SyncFence(presentFencePtr); + } + + /** + * Close the TransactionStats. Called by the framework when the listener returns. + * @hide + */ + public void close() { + mSyncFence.close(); + } + + /** + * Returns the timestamp 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; + } + + /** + * Returns a new SyncFence that signals when the transaction has been presented. + * The caller takes ownership of the fence and is responsible for closing + * it by calling {@link SyncFence#close}. + * If a device does not support present fences, an empty fence will be returned. + */ + @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) + public @NonNull SyncFence getPresentFence() { + return new SyncFence(mSyncFence); + } + }; + + /** * Threshold values that are sent with * {@link Transaction#setTrustedPresentationCallback(SurfaceControl, * TrustedPresentationThresholds, Executor, Consumer)} @@ -4185,12 +4237,35 @@ public final class SurfaceControl implements Parcelable { } /** - * Sets the frame timeline vsync id received from choreographer - * {@link Choreographer#getVsyncId()} that corresponds to the transaction submitted on that - * surface control. + * Sets the frame timeline to use in SurfaceFlinger. + * + * A frame timeline should be chosen based on the frame deadline the application + * can meet when rendering the frame and the application's desired presentation time. + * By setting a frame timeline, SurfaceFlinger tries to present the frame at the + * corresponding expected presentation time. + * + * To receive frame timelines, a callback must be posted to Choreographer using + * {@link Choreographer#postVsyncCallback} The vsyncId can then be extracted from the + * {@link Choreographer.FrameTimeline#getVsyncId}. + * + * @param vsyncId The vsync ID received from Choreographer, setting the frame's + * presentation target to the corresponding expected presentation time + * and deadline from the frame to be rendered. A stale or invalid value + * will be ignored. * - * @hide */ + @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) + @NonNull + public Transaction setFrameTimeline(long vsyncId) { + if (!Flags.sdkDesiredPresentTime()) { + Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled"); + return this; + } + nativeSetFrameTimelineVsync(mNativeObject, vsyncId); + return this; + } + + /** @hide */ @NonNull public Transaction setFrameTimelineVsync(long frameTimelineVsyncId) { nativeSetFrameTimelineVsync(mNativeObject, frameTimelineVsyncId); @@ -4207,6 +4282,9 @@ public final class SurfaceControl implements Parcelable { * to avoid dropping frames (overwriting transactions), and unable to use timestamps (Which * provide a more efficient solution), then this method provides a method to pace your * transaction application. + * The listener is invoked once the transaction is applied, and never again. Multiple + * listeners can be added to the same transaction, however the order the listeners will + * be called is not guaranteed. * * @param executor The executor that the callback should be invoked on. * @param listener The callback that will be invoked when the transaction has been @@ -4223,6 +4301,33 @@ public final class SurfaceControl implements Parcelable { } /** + * Request to add a TransactionCompletedListener. + * + * The listener is invoked when transaction is presented, and never again. Multiple + * listeners can be added to the same transaction, however the order the listeners will + * be called is not guaranteed. + * + * @param executor The executor that the callback should be invoked on. + * @param listener The callback that will be invoked when the transaction has been + * completed. + */ + @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) + @NonNull + public Transaction addTransactionCompletedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<TransactionStats> listener) { + + if (!Flags.sdkDesiredPresentTime()) { + Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled"); + return this; + } + Consumer<TransactionStats> listenerInner = stats -> executor.execute( + () -> listener.andThen(TransactionStats::close).accept(stats)); + nativeAddTransactionCompletedListener(mNativeObject, listenerInner); + return this; + } + + /** * Sets a callback to receive feedback about the presentation of a {@link SurfaceControl}. * When the {@link SurfaceControl} is presented according to the passed in * {@link TrustedPresentationThresholds}, it is said to "enter the state", and receives the @@ -4321,6 +4426,30 @@ 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. + * + * Transactions will not be presented until all of their acquire fences have signaled even + * if the app requests an earlier present time. + * + * If an earlier transaction has a desired present time of x, and a later transaction has + * 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. + * @return This transaction + */ + @FlaggedApi(Flags.FLAG_SDK_DESIRED_PRESENT_TIME) + @NonNull + public Transaction setDesiredPresentTime(long desiredPresentTime) { + if (!Flags.sdkDesiredPresentTime()) { + Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled"); + return this; + } + nativeSetDesiredPresentTime(mNativeObject, desiredPresentTime); + return this; + } + /** * Writes the transaction to parcel, clearing the transaction as if it had been applied so * it can be used to store future transactions. It's the responsibility of the parcel * reader to apply the original transaction. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8529b4e044fa..350876c828b7 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6943,7 +6943,11 @@ public final class ViewRootImpl implements ViewParent, } private int doOnBackKeyEvent(KeyEvent keyEvent) { - OnBackInvokedCallback topCallback = getOnBackInvokedDispatcher().getTopCallback(); + WindowOnBackInvokedDispatcher dispatcher = getOnBackInvokedDispatcher(); + OnBackInvokedCallback topCallback = dispatcher.getTopCallback(); + if (dispatcher.isDispatching()) { + return FINISH_NOT_HANDLED; + } if (topCallback instanceof OnBackAnimationCallback) { final OnBackAnimationCallback animationCallback = (OnBackAnimationCallback) topCallback; diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f61ed515b70e..d8fa41589f29 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1260,7 +1260,7 @@ public interface WindowManager extends ViewManager { * <p>When this compat override is enabled the min aspect ratio given in the app's manifest can * be overridden by the device manufacturer using their discretion to improve display * compatibility unless the app's manifest value is higher. This treatment will also apply if - * no min aspect ratio value is provided in the manifest. These treatments can apply only in + * no min aspect ratio value is provided in the manifest. These treatments can apply either in * specific cases (e.g. device is in portrait) or each time the app is displayed on screen. * * <p>Setting this property to {@code false} informs the system that the app must be @@ -1494,6 +1494,30 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"; /** + * Activity or Application level {@link android.content.pm.PackageManager.Property + * PackageManager.Property} for an app to declare that System UI should be shown for this + * app/component to allow it to be launched as multiple instances. This property only affects + * SystemUI behavior and does _not_ affect whether a component can actually be launched into + * multiple instances, which is determined by the Activity's {@code launchMode} or the launching + * Intent's flags. If the property is set on the Application, then all components within that + * application will use that value unless specified per component. + * + * The value must be a boolean string. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI" + * android:value="true|false"/> + * </activity> + * </pre> + */ + @FlaggedApi(Flags.FLAG_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI) + public static final String PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI = + "android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"; + + /** * Request for app's keyboard shortcuts to be retrieved asynchronously. * * @param receiver The callback to be triggered when the result is ready. diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index bb49679453ac..dbeffc89fa09 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -1469,7 +1469,8 @@ public final class AutofillManager { if (infos.size() == 0) { throw new IllegalArgumentException("No VirtualViewInfo found"); } - if (isCredmanRequested(view) && mIsFillAndSaveDialogDisabledForCredentialManager) { + if (shouldSuppressDialogsForCredman(view) + && mIsFillAndSaveDialogDisabledForCredentialManager) { if (sDebug) { Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:" + view.getAutofillId().toString()); @@ -1493,7 +1494,7 @@ public final class AutofillManager { * @hide */ public void notifyViewEnteredForFillDialog(View v) { - if (isCredmanRequested(v) + if (shouldSuppressDialogsForCredman(v) && mIsFillAndSaveDialogDisabledForCredentialManager) { if (sDebug) { Log.d(TAG, "Ignoring Fill Dialog request since important for credMan:" @@ -3390,19 +3391,39 @@ public final class AutofillManager { } } - private boolean isCredmanRequested(View view) { + private boolean shouldSuppressDialogsForCredman(View view) { if (view == null) { return false; } + // isCredential field indicates that the developer might be calling Credman, and we should + // suppress autofill dialogs. But it is not a good enough indicator that there is a valid + // credman option. if (view.isCredential()) { return true; } + return containsAutofillHintPrefix(view, View.AUTOFILL_HINT_CREDENTIAL_MANAGER); + } + + private boolean isCredmanRequested(View view) { + if (view == null) { + return false; + } + String[] hints = view.getAutofillHints(); + if (hints == null) { + return false; + } + // if hint starts with 'credential=', then we assume that there is a valid + // credential option set by the client. + return containsAutofillHintPrefix(view, View.AUTOFILL_HINT_CREDENTIAL_MANAGER + "="); + } + + private boolean containsAutofillHintPrefix(View view, String prefix) { String[] hints = view.getAutofillHints(); if (hints == null) { return false; } for (String hint : hints) { - if (hint != null && hint.startsWith(View.AUTOFILL_HINT_CREDENTIAL_MANAGER)) { + if (hint != null && hint.startsWith(prefix)) { return true; } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index feccc6bef7a4..3bc02a6c8fd1 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -354,7 +354,11 @@ public final class InputMethodManager { * @hide */ public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() { - forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper()); + // Skip this call if we are in system_server, as the system code should not use this + // deprecated instance. + if (!ActivityThread.isSystem()) { + forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper()); + } } private static final Object sLock = new Object(); diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java index e62d5c95a1f8..64fe66e36bc3 100644 --- a/core/java/android/window/TransitionFilter.java +++ b/core/java/android/window/TransitionFilter.java @@ -212,7 +212,9 @@ public final class TransitionFilter implements Parcelable { continue; } } - if (!matchesTopActivity(change.getTaskInfo())) continue; + if (!matchesTopActivity(change.getTaskInfo(), change.getActivityComponent())) { + continue; + } if (mModes != null) { boolean pass = false; for (int m = 0; m < mModes.length; ++m) { @@ -234,11 +236,15 @@ public final class TransitionFilter implements Parcelable { return false; } - private boolean matchesTopActivity(ActivityManager.RunningTaskInfo info) { + private boolean matchesTopActivity(ActivityManager.RunningTaskInfo taskInfo, + @Nullable ComponentName activityComponent) { if (mTopActivity == null) return true; - if (info == null) return false; - final ComponentName component = info.topActivity; - return mTopActivity.equals(component); + if (activityComponent != null) { + return mTopActivity.equals(activityComponent); + } else if (taskInfo != null) { + return mTopActivity.equals(taskInfo.topActivity); + } + return false; } /** Check if the request matches this filter. It may generate false positives */ @@ -247,7 +253,7 @@ public final class TransitionFilter implements Parcelable { if (mActivityType == ACTIVITY_TYPE_UNDEFINED) return true; return request.getTriggerTask() != null && request.getTriggerTask().getActivityType() == mActivityType - && matchesTopActivity(request.getTriggerTask()); + && matchesTopActivity(request.getTriggerTask(), null /* activityCmp */); } @Override diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 7c9340e72f72..bceb8726b1cb 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -44,6 +44,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.content.ComponentName; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -635,6 +636,7 @@ public final class TransitionInfo implements Parcelable { private @ColorInt int mBackgroundColor; private SurfaceControl mSnapshot = null; private float mSnapshotLuma; + private ComponentName mActivityComponent = null; public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) { mContainer = container; @@ -663,6 +665,7 @@ public final class TransitionInfo implements Parcelable { mBackgroundColor = in.readInt(); mSnapshot = in.readTypedObject(SurfaceControl.CREATOR); mSnapshotLuma = in.readFloat(); + mActivityComponent = in.readTypedObject(ComponentName.CREATOR); } private Change localRemoteCopy() { @@ -685,6 +688,7 @@ public final class TransitionInfo implements Parcelable { out.mBackgroundColor = mBackgroundColor; out.mSnapshot = mSnapshot != null ? new SurfaceControl(mSnapshot, "localRemote") : null; out.mSnapshotLuma = mSnapshotLuma; + out.mActivityComponent = mActivityComponent; return out; } @@ -780,6 +784,11 @@ public final class TransitionInfo implements Parcelable { mSnapshotLuma = luma; } + /** Sets the component-name of the container. Container must be an Activity. */ + public void setActivityComponent(@Nullable ComponentName component) { + mActivityComponent = component; + } + /** @return the container that is changing. May be null if non-remotable (eg. activity) */ @Nullable public WindowContainerToken getContainer() { @@ -913,6 +922,12 @@ public final class TransitionInfo implements Parcelable { return mSnapshotLuma; } + /** @return the component-name of this container (if it is an activity). */ + @Nullable + public ComponentName getActivityComponent() { + return mActivityComponent; + } + /** @hide */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -936,6 +951,7 @@ public final class TransitionInfo implements Parcelable { dest.writeInt(mBackgroundColor); dest.writeTypedObject(mSnapshot, flags); dest.writeFloat(mSnapshotLuma); + dest.writeTypedObject(mActivityComponent, flags); } @NonNull @@ -994,6 +1010,10 @@ public final class TransitionInfo implements Parcelable { if (mLastParent != null) { sb.append(" lastParent="); sb.append(mLastParent); } + if (mActivityComponent != null) { + sb.append(" component="); + sb.append(mActivityComponent.flattenToShortString()); + } sb.append('}'); return sb.toString(); } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 6a8ca339d60d..86804c6117c7 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -174,6 +174,21 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } } + /** + * Indicates if the dispatcher is actively dispatching to a callback. + */ + public boolean isDispatching() { + return mIsDispatching; + } + + private void onStartDispatching() { + mIsDispatching = true; + } + + private void onStopDispatching() { + mIsDispatching = false; + } + private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) { boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); if (isInProgress && callback instanceof OnBackAnimationCallback) { @@ -231,7 +246,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { .ImeOnBackInvokedCallback ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback) callback).getIOnBackInvokedCallback() - : new OnBackInvokedCallbackWrapper(callback); + : new OnBackInvokedCallbackWrapper(callback, this); callbackInfo = new OnBackInvokedCallbackInfo( iCallback, priority, @@ -258,6 +273,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @NonNull private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); + private boolean mIsDispatching = false; /** * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it @@ -317,18 +333,33 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } } final CallbackRef mCallbackRef; + /** + * The dispatcher this callback is registered with. + * This can be null for callbacks on {@link ImeOnBackInvokedDispatcher} because they are + * forwarded and registered on the app's {@link WindowOnBackInvokedDispatcher}. */ + @Nullable + private final WindowOnBackInvokedDispatcher mDispatcher; - OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { + OnBackInvokedCallbackWrapper( + @NonNull OnBackInvokedCallback callback, + WindowOnBackInvokedDispatcher dispatcher) { mCallbackRef = new CallbackRef(callback, true /* useWeakRef */); + mDispatcher = dispatcher; } - OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback, boolean useWeakRef) { + OnBackInvokedCallbackWrapper( + @NonNull OnBackInvokedCallback callback, + boolean useWeakRef) { mCallbackRef = new CallbackRef(callback, useWeakRef); + mDispatcher = null; } @Override public void onBackStarted(BackMotionEvent backEvent) { Handler.getMain().post(() -> { + if (mDispatcher != null) { + mDispatcher.onStartDispatching(); + } final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { mProgressAnimator.onBackStarted(backEvent, event -> @@ -353,6 +384,9 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackCancelled() { Handler.getMain().post(() -> { + if (mDispatcher != null) { + mDispatcher.onStopDispatching(); + } mProgressAnimator.onBackCancelled(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { @@ -365,6 +399,9 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackInvoked() throws RemoteException { Handler.getMain().post(() -> { + if (mDispatcher != null) { + mDispatcher.onStopDispatching(); + } boolean isInProgress = mProgressAnimator.isBackAnimationInProgress(); mProgressAnimator.reset(); final OnBackInvokedCallback callback = mCallbackRef.get(); diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 56df49370379..3794c5d647a4 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -63,4 +63,12 @@ flag { description: "Enable trustedPresentationListener on windows public API" is_fixed_read_only: true bug: "278027319" -}
\ No newline at end of file +} + +flag { + namespace: "window_surfaces" + name: "sdk_desired_present_time" + description: "Feature flag for the new SDK API to set desired present time" + is_fixed_read_only: true + bug: "295038072" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 3366a7ee23c0..f2bce9c44001 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -82,4 +82,12 @@ flag { description: "Enable record activity snapshot by default" bug: "259497289" is_fixed_read_only: true +} + +flag { + name: "supports_multi_instance_system_ui" + namespace: "multitasking" + description: "Feature flag to enable a multi-instance system ui component property." + bug: "262864589" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index 1bd098291a2a..eeea17bf39dd 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -68,10 +68,10 @@ public class SystemUiSystemPropertiesFlags { // TODO b/291899544: for released flags, use resource config values /** Value used by polite notif. feature */ public static final Flag NOTIF_COOLDOWN_T1 = devFlag( - "persist.debug.sysui.notification.notif_cooldown_t1", 5000); + "persist.debug.sysui.notification.notif_cooldown_t1", 60000); /** Value used by polite notif. feature */ public static final Flag NOTIF_COOLDOWN_T2 = devFlag( - "persist.debug.sysui.notification.notif_cooldown_t2", 3000); + "persist.debug.sysui.notification.notif_cooldown_t2", 5000); /** Value used by polite notif. feature */ public static final Flag NOTIF_VOLUME1 = devFlag( "persist.debug.sysui.notification.notif_volume1", 30); @@ -80,12 +80,6 @@ public class SystemUiSystemPropertiesFlags { /** Value used by polite notif. feature. -1 to ignore the counter */ public static final Flag NOTIF_COOLDOWN_COUNTER_RESET = devFlag( "persist.debug.sysui.notification.notif_cooldown_counter_reset", 10); - /** - * Value used by polite notif. feature: cooldown behavior/strategy. Valid values: rule1, - * rule2 - */ - public static final Flag NOTIF_COOLDOWN_RULE = devFlag( - "persist.debug.sysui.notification.notif_cooldown_rule", "rule1"); /** b/303716154: For debugging only: use short bitmap duration. */ public static final Flag DEBUG_SHORT_BITMAP_DURATION = devFlag( diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 1330e1681dc4..7a79e0f7cfea 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -977,11 +977,16 @@ public class BatteryStatsHistory { /** * @return true if there is more than 100MB free disk space left. */ + @android.ravenwood.annotation.RavenwoodReplace private boolean hasFreeDiskSpace() { final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath()); return stats.getAvailableBytes() > MIN_FREE_SPACE; } + private boolean hasFreeDiskSpace$ravenwood() { + return true; + } + @VisibleForTesting public List<String> getFilesNames() { List<String> names = new ArrayList<>(); diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java index 2298cbde10d1..ab982f5b67cf 100644 --- a/core/java/com/android/internal/os/PowerProfile.java +++ b/core/java/com/android/internal/os/PowerProfile.java @@ -50,6 +50,7 @@ import java.util.HashMap; * Customize the XML file for different devices. * [hidden] */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class PowerProfile { public static final String TAG = "PowerProfile"; @@ -321,6 +322,13 @@ public class PowerProfile { private int mCpuPowerBracketCount; @VisibleForTesting + public PowerProfile() { + synchronized (sLock) { + initLocked(); + } + } + + @VisibleForTesting @UnsupportedAppUsage public PowerProfile(Context context) { this(context, false); @@ -358,6 +366,10 @@ public class PowerProfile { if (sPowerItemMap.size() == 0 && sPowerArrayMap.size() == 0) { readPowerValuesFromXml(context, xmlId); } + initLocked(); + } + + private void initLocked() { initCpuClusters(); initCpuScalingPolicies(); initCpuPowerBrackets(); diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 54fdcc68fe59..4e3b64c0d4b9 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -47,6 +47,7 @@ import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources.Theme; @@ -389,10 +390,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context); mAllowFloatingWindowsFillScreen = context.getResources().getBoolean( com.android.internal.R.bool.config_allowFloatingWindowsFillScreen); - mEdgeToEdgeEnforced = - context.getApplicationInfo().targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION - || (CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE) - && Flags.enforceEdgeToEdge()); + mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(context.getApplicationInfo(), true /* local */); if (mEdgeToEdgeEnforced) { getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED; mDecorFitsSystemWindows = false; @@ -433,6 +431,22 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { mActivityConfigCallback = activityConfigCallback; } + /** + * Returns whether the given application is enforced to go edge-to-edge. + * + * @param info The application to query. + * @param local Whether this is called from the process of the given application. + * @return {@code true} if edge-to-edge is enforced. Otherwise, {@code false}. + */ + public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local) { + return info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION + || (Flags.enforceEdgeToEdge() && (local + // Calling this doesn't require a permission. + ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE) + // Calling this requires permissions. + : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))); + } + @Override public final void setContainer(Window container) { super.setContainer(container); diff --git a/core/java/com/android/internal/power/ModemPowerProfile.java b/core/java/com/android/internal/power/ModemPowerProfile.java index a555ae38736e..b15c10e6ba20 100644 --- a/core/java/com/android/internal/power/ModemPowerProfile.java +++ b/core/java/com/android/internal/power/ModemPowerProfile.java @@ -39,6 +39,7 @@ import java.util.Arrays; /** * ModemPowerProfile for handling the modem element in the power_profile.xml */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class ModemPowerProfile { private static final String TAG = "ModemPowerProfile"; diff --git a/core/java/com/android/server/OWNERS b/core/java/com/android/server/OWNERS deleted file mode 100644 index 1c2d19d94871..000000000000 --- a/core/java/com/android/server/OWNERS +++ /dev/null @@ -1 +0,0 @@ -per-file SystemConfig.java = file:/PACKAGE_MANAGER_OWNERS diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index 893cc98ff9b1..6f1c76385120 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -82,10 +82,16 @@ struct SQLiteConnection { const String8 path; const String8 label; + // The prepared statement used to determine which tables are updated by a statement. This + // is is initially null. It is set non-null on first use. + sqlite3_stmt* tableQuery; + volatile bool canceled; SQLiteConnection(sqlite3* db, int openFlags, const String8& path, const String8& label) : - db(db), openFlags(openFlags), path(path), label(label), canceled(false) { } + db(db), openFlags(openFlags), path(path), label(label), tableQuery(nullptr), + canceled(false) { } + }; // Called each time a statement begins execution, when tracing is enabled. @@ -188,6 +194,9 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { if (connection) { ALOGV("Closing connection %p", connection->db); + if (connection->tableQuery != nullptr) { + sqlite3_finalize(connection->tableQuery); + } int err = sqlite3_close(connection->db); if (err != SQLITE_OK) { // This can happen if sub-objects aren't closed first. Make sure the caller knows. @@ -419,6 +428,46 @@ static jboolean nativeIsReadOnly(JNIEnv* env, jclass clazz, jlong connectionPtr, return sqlite3_stmt_readonly(statement) != 0; } +static jboolean nativeUpdatesTempOnly(JNIEnv* env, jclass, + jlong connectionPtr, jlong statementPtr) { + sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr); + SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr); + + int result = SQLITE_OK; + if (connection->tableQuery == nullptr) { + static char const* sql = + "SELECT COUNT(*) FROM tables_used(?) WHERE schema != 'temp' AND wr != 0"; + result = sqlite3_prepare_v2(connection->db, sql, -1, &connection->tableQuery, nullptr); + if (result != SQLITE_OK) { + ALOGE("failed to compile query table: %s", + sqlite3_errstr(sqlite3_extended_errcode(connection->db))); + return false; + } + } + + // A temporary, to simplify the code. + sqlite3_stmt* query = connection->tableQuery; + sqlite3_reset(query); + sqlite3_clear_bindings(query); + result = sqlite3_bind_text(query, 1, sqlite3_sql(statement), -1, SQLITE_STATIC); + if (result != SQLITE_OK) { + ALOGE("tables bind pointer returns %s", sqlite3_errstr(result)); + return false; + } + result = sqlite3_step(query); + if (result != SQLITE_ROW) { + ALOGE("tables query error: %d/%s", result, sqlite3_errstr(result)); + // Make sure the query is no longer bound to the statement. + sqlite3_clear_bindings(query); + return false; + } + + int count = sqlite3_column_int(query, 0); + // Make sure the query is no longer bound to the statement SQL string. + sqlite3_clear_bindings(query); + return count == 0; +} + static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr); @@ -915,6 +964,8 @@ static const JNINativeMethod sMethods[] = (void*)nativeGetParameterCount }, { "nativeIsReadOnly", "(JJ)Z", (void*)nativeIsReadOnly }, + { "nativeUpdatesTempOnly", "(JJ)Z", + (void*)nativeUpdatesTempOnly }, { "nativeGetColumnCount", "(JJ)I", (void*)nativeGetColumnCount }, { "nativeGetColumnName", "(JJI)Ljava/lang/String;", diff --git a/core/jni/android_hardware_SyncFence.cpp b/core/jni/android_hardware_SyncFence.cpp index b99665346f25..6e94616db825 100644 --- a/core/jni/android_hardware_SyncFence.cpp +++ b/core/jni/android_hardware_SyncFence.cpp @@ -66,6 +66,10 @@ static jlong SyncFence_getSignalTime(JNIEnv* env, jobject, jlong jPtr) { return fromJlong<Fence>(jPtr)->getSignalTime(); } +static void SyncFence_incRef(JNIEnv*, jobject, jlong jPtr) { + fromJlong<Fence>(jPtr)->incStrong((void*)SyncFence_incRef); +} + // ---------------------------------------------------------------------------- // JNI Glue // ---------------------------------------------------------------------------- @@ -80,6 +84,7 @@ static const JNINativeMethod gMethods[] = { { "nGetFd", "(J)I", (void*) SyncFence_getFd }, { "nWait", "(JJ)Z", (void*) SyncFence_wait }, { "nGetSignalTime", "(J)J", (void*) SyncFence_getSignalTime }, + { "nIncRef", "(J)V", (void*) SyncFence_incRef }, }; // clang-format on diff --git a/core/jni/android_os_VintfObject.cpp b/core/jni/android_os_VintfObject.cpp index 1baea2aecc3c..b6517117ca62 100644 --- a/core/jni/android_os_VintfObject.cpp +++ b/core/jni/android_os_VintfObject.cpp @@ -46,6 +46,7 @@ using vintf::toXml; using vintf::Version; using vintf::VintfObject; using vintf::Vndk; +using vintf::CheckFlags::ENABLE_ALL_CHECKS; template<typename V> static inline jobjectArray toJavaStringArray(JNIEnv* env, const V& v) { @@ -93,12 +94,13 @@ static jobjectArray android_os_VintfObject_report(JNIEnv* env, jclass) return toJavaStringArray(env, cStrings); } -static jint android_os_VintfObject_verifyWithoutAvb(JNIEnv* env, jclass) { +static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv* env, jclass) { std::string error; - int32_t status = VintfObject::GetInstance()->checkCompatibility(&error, - ::android::vintf::CheckFlags::DISABLE_AVB_CHECK); + int32_t status = + VintfObject::GetInstance() + ->checkCompatibility(&error, ENABLE_ALL_CHECKS.disableAvb().disableKernel()); if (status) - LOG(WARNING) << "VintfObject.verifyWithoutAvb() returns " << status << ": " << error; + LOG(WARNING) << "VintfObject.verifyBuildAtBoot() returns " << status << ": " << error; return status; } @@ -170,7 +172,7 @@ static jobject android_os_VintfObject_getTargetFrameworkCompatibilityMatrixVersi static const JNINativeMethod gVintfObjectMethods[] = { {"report", "()[Ljava/lang/String;", (void*)android_os_VintfObject_report}, - {"verifyWithoutAvb", "()I", (void*)android_os_VintfObject_verifyWithoutAvb}, + {"verifyBuildAtBoot", "()I", (void*)android_os_VintfObject_verifyBuildAtBoot}, {"getHalNamesAndVersions", "()[Ljava/lang/String;", (void*)android_os_VintfObject_getHalNamesAndVersions}, {"getSepolicyVersion", "()Ljava/lang/String;", diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index db42246ca76c..55326da2c7df 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -231,6 +231,16 @@ static struct { static struct { jclass clazz; + jmethodID accept; +} gConsumerClassInfo; + +static struct { + jclass clazz; + jmethodID ctor; +} gTransactionStatsClassInfo; + +static struct { + jclass clazz; jmethodID ctor; jfieldID format; jfieldID alphaInterpretation; @@ -317,6 +327,52 @@ private: } }; +class TransactionCompletedListenerWrapper { +public: + explicit TransactionCompletedListenerWrapper(JNIEnv* env, jobject object) { + env->GetJavaVM(&mVm); + mTransactionCompletedListenerObject = env->NewGlobalRef(object); + LOG_ALWAYS_FATAL_IF(!mTransactionCompletedListenerObject, "Failed to make global ref"); + } + + ~TransactionCompletedListenerWrapper() { + getenv()->DeleteGlobalRef(mTransactionCompletedListenerObject); + } + + void callback(nsecs_t latchTime, const sp<Fence>& presentFence, + const std::vector<SurfaceControlStats>& /*stats*/) { + JNIEnv* env = getenv(); + // Adding a strong reference for java SyncFence + presentFence->incStrong(0); + + jobject stats = + env->NewObject(gTransactionStatsClassInfo.clazz, gTransactionStatsClassInfo.ctor, + latchTime, presentFence.get()); + env->CallVoidMethod(mTransactionCompletedListenerObject, gConsumerClassInfo.accept, stats); + env->DeleteLocalRef(stats); + DieIfException(env, "Uncaught exception in TransactionCompletedListener."); + } + + static void transactionCallbackThunk(void* context, nsecs_t latchTime, + const sp<Fence>& presentFence, + const std::vector<SurfaceControlStats>& stats) { + TransactionCompletedListenerWrapper* listener = + reinterpret_cast<TransactionCompletedListenerWrapper*>(context); + listener->callback(latchTime, presentFence, stats); + delete listener; + } + +private: + jobject mTransactionCompletedListenerObject; + JavaVM* mVm; + + JNIEnv* getenv() { + JNIEnv* env; + mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); + return env; + } +}; + class WindowInfosReportedListenerWrapper : public gui::BnWindowInfosReportedListener { public: explicit WindowInfosReportedListenerWrapper(JNIEnv* env, jobject listener) { @@ -1879,10 +1935,16 @@ static void nativeSetFrameTimelineVsync(JNIEnv* env, jclass clazz, jlong transac FrameTimelineInfo ftInfo; ftInfo.vsyncId = frameTimelineVsyncId; - ftInfo.inputEventId = android::os::IInputConstants::INVALID_INPUT_EVENT_ID; transaction->setFrameTimelineInfo(ftInfo); } +static void nativeSetDesiredPresentTime(JNIEnv* env, jclass clazz, jlong transactionObj, + jlong desiredPresentTime) { + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + + transaction->setDesiredPresentTime(desiredPresentTime); +} + static void nativeAddTransactionCommittedListener(JNIEnv* env, jclass clazz, jlong transactionObj, jobject transactionCommittedListenerObject) { auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); @@ -1894,6 +1956,17 @@ static void nativeAddTransactionCommittedListener(JNIEnv* env, jclass clazz, jlo context); } +static void nativeAddTransactionCompletedListener(JNIEnv* env, jclass clazz, jlong transactionObj, + jobject transactionCompletedListenerObject) { + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + + void* context = + new TransactionCompletedListenerWrapper(env, transactionCompletedListenerObject); + transaction->addTransactionCompletedCallback(TransactionCompletedListenerWrapper:: + transactionCallbackThunk, + context); +} + static void nativeSetTrustedPresentationCallback(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject, jlong trustedPresentationCallbackObject, @@ -2318,6 +2391,8 @@ static const JNINativeMethod sSurfaceControlMethods[] = { (void*)nativeSurfaceFlushJankData }, {"nativeAddTransactionCommittedListener", "(JLandroid/view/SurfaceControl$TransactionCommittedListener;)V", (void*) nativeAddTransactionCommittedListener }, + {"nativeAddTransactionCompletedListener", "(JLjava/util/function/Consumer;)V", + (void*) nativeAddTransactionCompletedListener }, {"nativeSetTrustedPresentationCallback", "(JJJLandroid/view/SurfaceControl$TrustedPresentationThresholds;)V", (void*) nativeSetTrustedPresentationCallback }, {"nativeClearTrustedPresentationCallback", "(JJ)V", @@ -2337,6 +2412,8 @@ static const JNINativeMethod sSurfaceControlMethods[] = { {"getNativeTrustedPresentationCallbackFinalizer", "()J", (void*)getNativeTrustedPresentationCallbackFinalizer }, {"nativeGetStalledTransactionInfo", "(I)Landroid/gui/StalledTransactionInfo;", (void*) nativeGetStalledTransactionInfo }, + {"nativeSetDesiredPresentTime", "(JJ)V", + (void*) nativeSetDesiredPresentTime }, // clang-format on }; @@ -2539,6 +2616,16 @@ int register_android_view_SurfaceControl(JNIEnv* env) gTransactionCommittedListenerClassInfo.onTransactionCommitted = GetMethodIDOrDie(env, transactionCommittedListenerClazz, "onTransactionCommitted", "()V"); + jclass consumerClazz = FindClassOrDie(env, "java/util/function/Consumer"); + gConsumerClassInfo.clazz = MakeGlobalRefOrDie(env, consumerClazz); + gConsumerClassInfo.accept = + GetMethodIDOrDie(env, consumerClazz, "accept", "(Ljava/lang/Object;)V"); + + jclass transactionStatsClazz = + FindClassOrDie(env, "android/view/SurfaceControl$TransactionStats"); + gTransactionStatsClassInfo.clazz = MakeGlobalRefOrDie(env, transactionStatsClazz); + gTransactionStatsClassInfo.ctor = + GetMethodIDOrDie(env, gTransactionStatsClassInfo.clazz, "<init>", "(JJ)V"); jclass displayDecorationSupportClazz = FindClassOrDie(env, "android/hardware/graphics/common/DisplayDecorationSupport"); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 1eeffb9e2875..ef6caefd3daf 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7147,6 +7147,16 @@ android:label="@string/permlab_foregroundServiceFileManagement" android:protectionLevel="normal|instant" /> + <!-- @FlaggedApi("android.content.pm.introduce_media_processing_type") + Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "mediaProcessing". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING" + android:description="@string/permdesc_foregroundServiceMediaProcessing" + android:label="@string/permlab_foregroundServiceMediaProcessing" + android:protectionLevel="normal|instant" /> + <!-- Allows a regular application to use {@link android.app.Service#startForeground Service.startForeground} with the type "specialUse". <p>Protection level: normal|appop|instant diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index a23201e820aa..8fae6db4114a 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1744,6 +1744,12 @@ TODO: b/258855262 mark this field as {@code hide} once this bug is fixed. <flag name="fileManagement" value="0x1000" /> --> + <!-- Media processing use cases such as video or photo editing and processing. + <p>Requires the app to hold the permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PROCESSING} in order to use + this type. + --> + <flag name="mediaProcessing" value="0x2000" /> <!-- Use cases that can't be categorized into any other foreground service types, but also can't use @link android.app.job.JobInfo.Builder} APIs. See {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE} for the diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 806be9471ae5..5be29a6d68b8 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2349,6 +2349,8 @@ <string name="config_defaultCallRedirection" translatable="false"></string> <!-- The name of the package that will hold the call screening role by default. --> <string name="config_defaultCallScreening" translatable="false"></string> + <!-- The name of the package that will hold the wallet role by default. --> + <string name="config_defaultWallet" translatable="false" /> <!-- The name of the package that will hold the system gallery role. --> <string name="config_systemGallery" translatable="false">com.android.gallery3d</string> <!-- The names of the packages that will hold the automotive projection role. --> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 8d80af41680a..53464547c272 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -212,6 +212,36 @@ <bool name="config_send_satellite_datagram_to_modem_in_demo_mode">false</bool> <java-symbol type="bool" name="config_send_satellite_datagram_to_modem_in_demo_mode" /> + <!-- List of country codes where oem-enabled satellite services are either allowed or disallowed + by the device. Each country code is a lowercase 2 character ISO-3166-1 alpha-2. + --> + <string-array name="config_oem_enabled_satellite_country_codes"> + </string-array> + <java-symbol type="array" name="config_oem_enabled_satellite_country_codes" /> + + <!-- The file storing S2-cell-based satellite access restriction of the countries defined by + config_oem_enabled_satellite_countries. --> + <string name="config_oem_enabled_satellite_s2cell_file"></string> + <java-symbol type="string" name="config_oem_enabled_satellite_s2cell_file" /> + + <!-- Whether to treat the countries defined by the config_oem_enabled_satellite_countries + as satellite-allowed areas. The default true value means the countries defined by + config_oem_enabled_satellite_countries will be treated as satellite-allowed areas. + --> + <bool name="config_oem_enabled_satellite_access_allow">true</bool> + <java-symbol type="bool" name="config_oem_enabled_satellite_access_allow" /> + + <!-- The time duration in seconds which is used to decide whether the Location returned from + LocationManager#getLastKnownLocation is fresh. + + The Location is considered fresh if the duration from the Location's elapsed real time to + the current elapsed real time is less than this config. If the Location is considered + fresh, it will be used as the current location by Telephony to decide whether satellite + services should be allowed. + --> + <integer name="config_oem_enabled_satellite_location_fresh_duration">600</integer> + <java-symbol type="integer" name="config_oem_enabled_satellite_location_fresh_duration" /> + <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks will not perform handover if the target transport is out of service, or VoPS not supported. The network will be torn down on the source transport, and will be diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 17bb86a7c423..53b473e0ac1f 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -130,6 +130,8 @@ <staging-public-group type="string" first-id="0x01ba0000"> <!-- @hide @SystemApi @FlaggedApi("android.permission.flags.retail_demo_role_enabled") --> <public name="config_defaultRetailDemo" /> + <!-- @hide @SystemApi @FlaggedApi("android.permission.flags.wallet_role_enabled") --> + <public name="config_defaultWallet" /> </staging-public-group> <staging-public-group type="dimen" first-id="0x01b90000"> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d2fb9e12d069..542e9d6f936f 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1248,6 +1248,11 @@ <string name="permdesc_foregroundServiceFileManagement">Allows the app to make use of foreground services with the type \"fileManagement\"</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_foregroundServiceMediaProcessing">run foreground service with the type \"mediaProcessing\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceMediaProcessing">Allows the app to make use of foreground services with the type \"mediaProcessing\"</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_foregroundServiceSpecialUse">run foreground service with the type \"specialUse\"</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_foregroundServiceSpecialUse">Allows the app to make use of foreground services with the type \"specialUse\"</string> diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java index 43266a51502b..cb3f99c37a4f 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java @@ -17,6 +17,7 @@ package android.animation; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.util.PollingCheck; @@ -343,6 +344,20 @@ public class AnimatorSetCallsTest { } @Test + public void childAnimatorCancelsDuringUpdate_animatorSetIsEnded() throws Throwable { + mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + animation.cancel(); + } + }); + mActivity.runOnUiThread(() -> { + mSet1.start(); + assertFalse(mSet1.isRunning()); + }); + } + + @Test public void reentrantStart() throws Throwable { CountDownLatch latch = new CountDownLatch(3); AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java index 1925588e8904..9d85b65d4dac 100644 --- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java +++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java @@ -16,6 +16,8 @@ package android.app; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; @@ -26,6 +28,8 @@ 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; @@ -226,4 +230,66 @@ 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/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index 3fc08ee6fd2e..bd5f809dc689 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -26,6 +26,9 @@ import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.test.AndroidTestCase; import android.util.Log; @@ -35,6 +38,7 @@ import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +57,10 @@ import java.util.concurrent.TimeUnit; @SmallTest public class SQLiteDatabaseTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String TAG = "SQLiteDatabaseTest"; private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); @@ -347,4 +355,50 @@ public class SQLiteDatabaseTest { assertTrue("ReadThread failed with errors: " + errors, errors.isEmpty()); } + + @RequiresFlagsEnabled(Flags.FLAG_SQLITE_ALLOW_TEMP_TABLES) + @Test + public void testTempTable() { + boolean allowed; + allowed = true; + mDatabase.beginTransactionReadOnly(); + try { + mDatabase.execSQL("CREATE TEMP TABLE t1 (i int, j int);"); + mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (2, 20)"); + mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (3, 30)"); + + final String sql = "SELECT i FROM t1 WHERE j = 30"; + try (SQLiteRawStatement s = mDatabase.createRawStatement(sql)) { + assertTrue(s.step()); + assertEquals(3, s.getColumnInt(0)); + } + + } catch (SQLiteException e) { + allowed = false; + } finally { + mDatabase.endTransaction(); + } + assertTrue(allowed); + + // Repeat the test on the main schema. + allowed = true; + mDatabase.beginTransactionReadOnly(); + try { + mDatabase.execSQL("CREATE TABLE t2 (i int, j int);"); + mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)"); + mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)"); + + final String sql = "SELECT i FROM t2 WHERE j = 30"; + try (SQLiteRawStatement s = mDatabase.createRawStatement(sql)) { + assertTrue(s.step()); + assertEquals(3, s.getColumnInt(0)); + } + + } catch (SQLiteException e) { + allowed = false; + } finally { + mDatabase.endTransaction(); + } + assertFalse(allowed); + } } diff --git a/core/tests/coretests/src/android/os/BuildTest.java b/core/tests/coretests/src/android/os/BuildTest.java index 3162e6da15c5..2d3e12331e23 100644 --- a/core/tests/coretests/src/android/os/BuildTest.java +++ b/core/tests/coretests/src/android/os/BuildTest.java @@ -45,7 +45,8 @@ public class BuildTest { public final RavenwoodRule mRavenwood = new RavenwoodRule(); @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.NULL_DEFAULT); /** * Asserts that a String is non-null and non-empty. If it is not, diff --git a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java index c9536b9b8129..533b799341c1 100644 --- a/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java +++ b/core/tests/coretests/src/com/android/internal/os/LongArrayMultiStateCounterTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertThrows; import android.os.BadParcelableException; import android.os.Parcel; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; @@ -34,7 +33,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -@IgnoreUnderRavenwood(blockedBy = LongArrayMultiStateCounter.class) public class LongArrayMultiStateCounterTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 43683ffad432..ce2543a47cf5 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -56,6 +56,7 @@ <permission name="android.permission.REAL_GET_TASKS"/> <permission name="android.permission.REQUEST_NETWORK_SCORES"/> <permission name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE"/> + <permission name="android.permission.SATELLITE_COMMUNICATION"/> <permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/> <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" /> <permission name="android.permission.START_ACTIVITY_AS_CALLER"/> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index b9efe65d2754..a1ea2b8ce032 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -134,6 +134,7 @@ applications that come with the platform <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/> <permission name="android.permission.DUMP"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> + <permission name="android.permission.LOCATION_BYPASS"/> <permission name="android.permission.LOCAL_MAC_ADDRESS"/> <permission name="android.permission.MANAGE_USERS"/> <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS" /> @@ -149,6 +150,7 @@ applications that come with the platform <permission name="android.permission.REGISTER_CALL_PROVIDER"/> <permission name="android.permission.REGISTER_SIM_SUBSCRIPTION"/> <permission name="android.permission.REGISTER_STATS_PULL_ATOM"/> + <permission name="android.permission.SATELLITE_COMMUNICATION"/> <permission name="android.permission.SEND_RESPOND_VIA_MESSAGE"/> <permission name="android.permission.SHUTDOWN"/> <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/> diff --git a/data/keyboards/Vendor_0957_Product_0031.kl b/data/keyboards/Vendor_0957_Product_0031.kl new file mode 100644 index 000000000000..b47ee58c0c10 --- /dev/null +++ b/data/keyboards/Vendor_0957_Product_0031.kl @@ -0,0 +1,82 @@ +# 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. +# +# Key Layout file for Google Reference RCU Remote with customizable button. +# + +key 116 TV_POWER WAKE +key 217 ASSIST WAKE +key 423 MACRO_1 WAKE + +key 103 DPAD_UP +key 108 DPAD_DOWN +key 105 DPAD_LEFT +key 106 DPAD_RIGHT +key 353 DPAD_CENTER + +key 158 BACK +key 172 HOME WAKE + +key 113 VOLUME_MUTE +key 114 VOLUME_DOWN +key 115 VOLUME_UP + +key 2 1 +key 3 2 +key 4 3 +key 5 4 +key 6 5 +key 7 6 +key 8 7 +key 9 8 +key 10 9 +key 11 0 + +# custom keys +key usage 0x000c01BB TV_INPUT + +key usage 0x000c0185 TV_TELETEXT +key usage 0x000c0061 CAPTIONS + +key usage 0x000c01BD INFO +key usage 0x000c0037 PERIOD + +key usage 0x000c0069 PROG_RED +key usage 0x000c006A PROG_GREEN +key usage 0x000c006C PROG_YELLOW +key usage 0x000c006B PROG_BLUE +key usage 0x000c00B4 MEDIA_SKIP_BACKWARD +key usage 0x000c00CD MEDIA_PLAY_PAUSE +key usage 0x000c00B2 MEDIA_RECORD +key usage 0x000c00B3 MEDIA_SKIP_FORWARD + +key usage 0x000c022A BOOKMARK +key usage 0x000c01A2 ALL_APPS +key usage 0x000c019C PROFILE_SWITCH + +key usage 0x000c0096 SETTINGS +key usage 0x000c009F NOTIFICATION + +key usage 0x000c008D GUIDE +key usage 0x000c0089 TV + +key usage 0x000c0187 FEATURED_APP_1 WAKE #FreeTv + +key usage 0x000c009C CHANNEL_UP +key usage 0x000c009D CHANNEL_DOWN + +key usage 0x000c0077 BUTTON_3 WAKE #YouTube +key usage 0x000c0078 BUTTON_4 WAKE #Netflix +key usage 0x000c0079 BUTTON_6 WAKE +key usage 0x000c007A BUTTON_7 WAKE
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 592f9a57884c..80afb16d5832 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -382,9 +382,13 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { if (splitAttributes == null) { return TaskFragmentAnimationParams.DEFAULT; } - return new TaskFragmentAnimationParams.Builder() - // TODO(b/263047900): Update extensions API. - // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) - .build(); + final AnimationBackground animationBackground = splitAttributes.getAnimationBackground(); + if (animationBackground instanceof AnimationBackground.ColorBackground colorBackground) { + return new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(colorBackground.getColor()) + .build(); + } else { + return TaskFragmentAnimationParams.DEFAULT; + } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 6f356fa35d41..8b7fd108f031 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -893,8 +893,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return new SplitAttributes.Builder() .setSplitType(splitTypeToUpdate) .setLayoutDirection(splitAttributes.getLayoutDirection()) - // TODO(b/263047900): Update extensions API. - // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + .setAnimationBackground(splitAttributes.getAnimationBackground()) .build(); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index 60beb0b7f0a4..f471af052bf2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -25,6 +25,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.AnimationBackground; import androidx.window.extensions.embedding.SplitAttributes; import org.junit.Before; @@ -70,7 +71,7 @@ public class WindowExtensionsTest { .isEqualTo(SplitAttributes.LayoutDirection.LOCALE); assertThat(splitAttributes.getSplitType()) .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f)); - // TODO(b/263047900): Update extensions API. - // assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0); + assertThat(splitAttributes.getAnimationBackground()) + .isEqualTo(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT); } } diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml index 681a52bea2b2..e04ab817215c 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml @@ -21,4 +21,9 @@ android:orientation="vertical" android:id="@+id/bubble_bar_expanded_view"> + <com.android.wm.shell.bubbles.bar.BubbleBarHandleView + android:id="@+id/bubble_bar_handle_view" + android:layout_height="wrap_content" + android:layout_width="wrap_content" /> + </com.android.wm.shell.bubbles.bar.BubbleBarExpandedView> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index ac75c73d7e6d..06210ff98642 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation; import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; @@ -330,6 +331,9 @@ class ActivityEmbeddingAnimationRunner { if (!animation.hasExtension()) { continue; } + if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)) { + continue; + } final TransitionInfo.Change change = adapter.mChange; if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) { // Need to screenshot after startTransaction is applied otherwise activity 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 a49823648d01..81d963877e4c 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 @@ -403,8 +403,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mCurrentTracker.updateStartLocation(); // Dispatch onBackStarted, only to app callbacks. // System callbacks will receive onBackStarted when the remote animation starts. - if (!shouldDispatchToAnimator()) { - tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); + if (!shouldDispatchToAnimator() && mActiveCallback != null) { + tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); } } @@ -507,7 +507,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); // App is handling back animation. Cancel system animation latency tracking. cancelLatencyTracking(); - tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); + tryDispatchAppOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); } } @@ -551,14 +551,24 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && mBackNavigationInfo.isPrepareRemoteAnimation(); } - private void tryDispatchOnBackStarted(IOnBackInvokedCallback callback, + private void tryDispatchAppOnBackStarted( + IOnBackInvokedCallback callback, BackMotionEvent backEvent) { - if (callback == null || mOnBackStartDispatched) { + if (mOnBackStartDispatched && callback != null) { + return; + } + dispatchOnBackStarted(callback, backEvent); + mOnBackStartDispatched = true; + } + + private void dispatchOnBackStarted( + IOnBackInvokedCallback callback, + BackMotionEvent backEvent) { + if (callback == null) { return; } try { callback.onBackStarted(backEvent); - mOnBackStartDispatched = true; } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } @@ -940,9 +950,17 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (apps.length >= 1) { mCurrentTracker.updateStartLocation(); - tryDispatchOnBackStarted( - mActiveCallback, - mCurrentTracker.createStartEvent(apps[0])); + BackMotionEvent startEvent = + mCurrentTracker.createStartEvent(apps[0]); + // {@code mActiveCallback} is the callback from + // the BackAnimationRunners and not a real app-side + // callback. We also dispatch to the app-side callback + // (which should be a system callback with PRIORITY_SYSTEM) + // to keep consistent with app registered callbacks. + dispatchOnBackStarted(mActiveCallback, startEvent); + tryDispatchAppOnBackStarted( + mBackNavigationInfo.getOnBackInvokedCallback(), + startEvent); } // Dispatch the first progress after animation start for diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 470a82511481..ca283128891c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1177,14 +1177,17 @@ public class BubbleStackView extends FrameLayout if (mStackAnimationController.isStackOnLeftSide()) { int availableRectOffsetX = mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left; - animate().translationX(-(mBubbleSize + availableRectOffsetX)).start(); + mBubbleContainer + .animate() + .translationX(-(mBubbleSize + availableRectOffsetX)) + .start(); } else { int availableRectOffsetX = mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right; - animate().translationX(mBubbleSize - availableRectOffsetX).start(); + mBubbleContainer.animate().translationX(mBubbleSize - availableRectOffsetX).start(); } } else { - animate().translationX(0).start(); + mBubbleContainer.animate().translationX(0).start(); } }; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index d073f1df938a..66c0c9640477 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -70,7 +70,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private @Nullable Supplier<Rect> mLayerBoundsSupplier; private @Nullable Listener mListener; - private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext()); + private BubbleBarHandleView mHandleView; private @Nullable TaskView mTaskView; private @Nullable BubbleOverflowContainerView mOverflowView; @@ -111,7 +111,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation)); mCaptionHeight = context.getResources().getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_caption_height); - addView(mHandleView); + mHandleView = findViewById(R.id.bubble_bar_handle_view); applyThemeAttrs(); setClipToOutline(true); setOutlineProvider(new ViewOutlineProvider() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index 0693543515b4..662f325be38c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -16,7 +16,7 @@ package com.android.wm.shell.common.split; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED; import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; @@ -24,19 +24,27 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.PendingIntent; -import android.content.Context; +import android.content.ComponentName; import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.util.ArrayUtils; import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; +import java.util.Arrays; +import java.util.List; + /** Helper utility class for split screen components to use. */ public class SplitScreenUtils { /** Reverse the split position. */ @@ -135,4 +143,28 @@ public class SplitScreenUtils { return isLandscape; } } + + /** Returns the component from a PendingIntent */ + @Nullable + public static ComponentName getComponent(@Nullable PendingIntent pendingIntent) { + if (pendingIntent == null || pendingIntent.getIntent() == null) { + return null; + } + return pendingIntent.getIntent().getComponent(); + } + + /** Returns the component from a shortcut */ + @Nullable + public static ComponentName getShortcutComponent(@NonNull String packageName, String shortcutId, + @NonNull UserHandle user, @NonNull LauncherApps launcherApps) { + LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery(); + query.setPackage(packageName); + query.setShortcutIds(Arrays.asList(shortcutId)); + query.setQueryFlags(FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED); + List<ShortcutInfo> shortcuts = launcherApps.getShortcuts(query, user); + ShortcutInfo info = shortcuts != null && shortcuts.size() > 0 + ? shortcuts.get(0) + : null; + return info != null ? info.getActivity() : null; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 0ef047f44909..36f06e8bdb3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -210,7 +210,6 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RecentsTransitionHandler recentsTransitionHandler, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { if (DesktopModeStatus.isEnabled()) { return new DesktopModeWindowDecorViewModel( @@ -226,7 +225,6 @@ public abstract class WMShellModule { syncQueue, transitions, desktopTasksController, - recentsTransitionHandler, rootTaskDisplayAreaOrganizer); } return new CaptionWindowDecorViewModel( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 4a1bcaa7168a..b1c43c19366d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -320,9 +320,8 @@ class DesktopTasksController( } /** Move a task with given `taskId` to fullscreen */ - fun moveToFullscreen(taskId: Int, windowDecor: DesktopModeWindowDecoration) { + fun moveToFullscreen(taskId: Int) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> - windowDecor.incrementRelayoutBlock() moveToFullscreenWithAnimation(task, task.positionInParent) } } @@ -906,20 +905,17 @@ class DesktopTasksController( * @param position position of surface when drag ends. * @param inputCoordinate the coordinates of the motion event * @param taskBounds the updated bounds of the task being dragged. - * @param windowDecor the window decoration for the task being dragged */ fun onDragPositioningEnd( taskInfo: RunningTaskInfo, position: Point, inputCoordinate: PointF, - taskBounds: Rect, - windowDecor: DesktopModeWindowDecoration + taskBounds: Rect ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return } if (taskBounds.top <= transitionAreaHeight) { - windowDecor.incrementRelayoutBlock() moveToFullscreenWithAnimation(taskInfo, position) } if (inputCoordinate.x <= transitionAreaWidth) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index 9debb25ff296..0218493589b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -54,8 +54,6 @@ class ToggleResizeDesktopTaskTransitionHandler( taskId: Int, windowDecoration: DesktopModeWindowDecoration ) { - // Pause relayout until the transition animation finishes. - windowDecoration.incrementRelayoutBlock() transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this) taskToDecorationMap.put(taskId, windowDecoration) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 6b6a7bc42046..ffcc526eac40 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -112,7 +112,6 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs onChangeTransitionReady(change, startT, finishT); break; } - mWindowDecorViewModel.onTransitionReady(transition, info, change); } mTransitionToTaskInfo.put(transition, taskInfoList); } @@ -153,8 +152,6 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { - mWindowDecorViewModel.onTransitionMerged(merged, playing); - final List<ActivityManager.RunningTaskInfo> infoOfMerged = mTransitionToTaskInfo.get(merged); if (infoOfMerged == null) { @@ -178,7 +175,6 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs final List<ActivityManager.RunningTaskInfo> taskInfo = mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList()); mTransitionToTaskInfo.remove(transition); - mWindowDecorViewModel.onTransitionFinished(transition); for (int i = 0; i < taskInfo.size(); ++i) { mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index 3906599b7581..8b3de6298b2a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -52,9 +52,10 @@ interface IPip { * @param componentName ComponentName represents the Activity * @param destinationBounds the destination bounds the PiP window lands into * @param overlay an optional overlay to fade out after entering PiP + * @param appBounds the bounds used to set the buffer size of the optional content overlay */ oneway void stopSwipePipToHome(int taskId, in ComponentName componentName, - in Rect destinationBounds, in SurfaceControl overlay) = 2; + in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2; /** * Notifies the swiping Activity to PiP onto home transition is aborted diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 3635165d76ce..a9a3f788cb7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -334,6 +334,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @Nullable SurfaceControl mPipOverlay; + /** + * The app bounds used for the buffer size of the + * {@link com.android.wm.shell.pip.PipContentOverlay.PipAppIconOverlay}. + * + * Note that this is empty if the overlay is removed or if it's some other type of overlay + * defined in {@link PipContentOverlay}. + */ + @NonNull + final Rect mAppBounds = new Rect(); + public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @@ -464,15 +474,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. */ public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay) { + SurfaceControl overlay, Rect appBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "stopSwipePipToHome: %s, state=%s", componentName, mPipTransitionState); + "stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState); // do nothing if there is no startSwipePipToHome being called before if (!mPipTransitionState.getInSwipePipToHomeTransition()) { return; } mPipBoundsState.setBounds(destinationBounds); - mPipOverlay = overlay; + setContentOverlay(overlay, appBounds); if (ENABLE_SHELL_TRANSITIONS && overlay != null) { // With Shell transition, the overlay was attached to the remote transition leash, which // will be removed when the current transition is finished, so we need to reparent it @@ -1888,7 +1898,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, "%s: trying to remove overlay (%s) which is not local reference (%s)", TAG, surface, mPipOverlay); } - mPipOverlay = null; + clearContentOverlay(); } if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Avoid double removal, which is fatal. @@ -1905,6 +1915,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (callback != null) callback.run(); } + void clearContentOverlay() { + mPipOverlay = null; + mAppBounds.setEmpty(); + } + + void setContentOverlay(@Nullable SurfaceControl leash, @NonNull Rect appBounds) { + mPipOverlay = leash; + if (mPipOverlay != null) { + mAppBounds.set(appBounds); + } else { + mAppBounds.setEmpty(); + } + } + private void resetShadowRadius() { if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // mLeash is undefined when in PipTransitionState.UNDEFINED diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index f5f15d81ea44..8e375a9ef5ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -141,8 +141,6 @@ public class PipTransition extends PipTransitionController { /** Whether the PIP window has fade out for fixed rotation. */ private boolean mHasFadeOut; - private Rect mInitBounds = new Rect(); - /** Used for setting transform to a transaction from animator. */ private final PipAnimationController.PipTransactionHandler mTransactionConsumer = new PipAnimationController.PipTransactionHandler() { @@ -465,12 +463,13 @@ public class PipTransition extends PipTransitionController { mSurfaceTransactionHelper.crop(tx, leash, destinationBounds) .resetScale(tx, leash, destinationBounds) .round(tx, leash, true /* applyCornerRadius */); - if (mPipOrganizer.mPipOverlay != null && !mInitBounds.isEmpty()) { + final Rect appBounds = mPipOrganizer.mAppBounds; + if (mPipOrganizer.mPipOverlay != null && !appBounds.isEmpty()) { // Resetting the scale for pinned task while re-adjusting its crop, // also scales the overlay. So we need to update the overlay leash too. Rect overlayBounds = new Rect(destinationBounds); final int overlaySize = PipContentOverlay.PipAppIconOverlay - .getOverlaySize(mInitBounds, destinationBounds); + .getOverlaySize(appBounds, destinationBounds); overlayBounds.offsetTo( (destinationBounds.width() - overlaySize) / 2, @@ -479,7 +478,6 @@ public class PipTransition extends PipTransitionController { mPipOrganizer.mPipOverlay, overlayBounds); } } - mInitBounds.setEmpty(); wct.setBoundsChangeTransaction(taskInfo.token, tx); } final int displayRotation = taskInfo.getConfiguration().windowConfiguration @@ -617,7 +615,7 @@ public class PipTransition extends PipTransitionController { // if overlay is present remove it immediately, as exit transition came before it faded out if (mPipOrganizer.mPipOverlay != null) { startTransaction.remove(mPipOrganizer.mPipOverlay); - clearPipOverlay(); + mPipOrganizer.clearContentOverlay(); } if (pipChange == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -951,9 +949,6 @@ public class PipTransition extends PipTransitionController { final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = pipChange.getStartAbsBounds(); - // Cache the start bounds for overlay manipulations as a part of finishCallback. - mInitBounds.set(currentBounds); - int rotationDelta = deltaRotation(startRotation, endRotation); Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( taskInfo.pictureInPictureParams, currentBounds, destinationBounds); @@ -992,10 +987,12 @@ public class PipTransition extends PipTransitionController { 0 /* startingAngle */, rotationDelta); if (sourceHintRect == null) { // We use content overlay when there is no source rect hint to enter PiP use bounds - // animation. + // animation. We also temporarily disallow app icon overlay and use color overlay + // instead when in fixed rotation enter PiP in button nav with no sourceRectHint. + // TODO(b/319286295): Fix App Icon Overlay animation in fixed rotation in btn nav. // TODO(b/272819817): cleanup the null-check and extra logging. final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null; - if (hasTopActivityInfo) { + if (hasTopActivityInfo && mFixedRotationState != FIXED_ROTATION_TRANSITION) { animator.setAppIconContentOverlay( mContext, currentBounds, destinationBounds, taskInfo.topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); @@ -1022,7 +1019,7 @@ public class PipTransition extends PipTransitionController { } else { throw new RuntimeException("Unrecognized animation type: " + enterAnimationType); } - mPipOrganizer.mPipOverlay = animator.getContentOverlayLeash(); + mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration); @@ -1073,10 +1070,6 @@ public class PipTransition extends PipTransitionController { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "%s: SwipePipToHome should not use fixed rotation %d", TAG, mEndFixedRotation); } - Rect appBounds = pipTaskInfo.configuration.windowConfiguration.getAppBounds(); - if (mFixedRotationState == FIXED_ROTATION_CALLBACK && appBounds != null) { - mInitBounds.set(appBounds); - } final SurfaceControl swipePipToHomeOverlay = mPipOrganizer.mPipOverlay; if (swipePipToHomeOverlay != null) { // Launcher fade in the overlay on top of the fullscreen Task. It is possible we @@ -1106,7 +1099,7 @@ public class PipTransition extends PipTransitionController { sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); if (swipePipToHomeOverlay != null) { mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay, - this::clearPipOverlay /* callback */, false /* withStartDelay */); + null /* callback */, false /* withStartDelay */); } mPipTransitionState.setInSwipePipToHomeTransition(false); } @@ -1250,10 +1243,6 @@ public class PipTransition extends PipTransitionController { mPipMenuController.updateMenuBounds(destinationBounds); } - private void clearPipOverlay() { - mPipOrganizer.mPipOverlay = null; - } - @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 63f20fd8e997..238e6b5bf2af 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -982,8 +982,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, - SurfaceControl overlay) { - mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay); + SurfaceControl overlay, Rect appBounds) { + mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay, + appBounds); } private void abortSwipePipToHome(int taskId, ComponentName componentName) { @@ -1280,13 +1281,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb @Override public void stopSwipePipToHome(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay) { + Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { if (overlay != null) { overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); } executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", (controller) -> controller.stopSwipePipToHome( - taskId, componentName, destinationBounds, overlay)); + taskId, componentName, destinationBounds, overlay, appBounds)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 7b5709769369..880d95286de6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -23,12 +23,15 @@ import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenUtils.getComponent; +import static com.android.wm.shell.common.split.SplitScreenUtils.getShortcutComponent; import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.samePackage; @@ -47,6 +50,8 @@ import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; @@ -171,6 +176,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final ShellTaskOrganizer mTaskOrganizer; private final SyncTransactionQueue mSyncQueue; private final Context mContext; + private final PackageManager mPackageManager; + private final LauncherApps mLauncherApps; private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellExecutor mMainExecutor; private final SplitScreenImpl mImpl = new SplitScreenImpl(); @@ -186,7 +193,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final Optional<WindowDecorViewModel> mWindowDecorViewModel; private final Optional<DesktopTasksController> mDesktopTasksController; private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; - private final String[] mAppsSupportMultiInstances; + // A static allow list of apps which support multi-instance + private final String[] mAppsSupportingMultiInstance; @VisibleForTesting StageCoordinator mStageCoordinator; @@ -220,6 +228,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; + mPackageManager = context.getPackageManager(); + mLauncherApps = context.getSystemService(LauncherApps.class); mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; mDisplayController = displayController; @@ -242,7 +252,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, // TODO(255224696): Remove the config once having a way for client apps to opt-in // multi-instances split. - mAppsSupportMultiInstances = mContext.getResources() + mAppsSupportingMultiInstance = mContext.getResources() .getStringArray(R.array.config_appsSupportMultiInstancesSplit); } @@ -266,12 +276,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, WindowDecorViewModel windowDecorViewModel, DesktopTasksController desktopTasksController, ShellExecutor mainExecutor, - StageCoordinator stageCoordinator) { + StageCoordinator stageCoordinator, + String[] appsSupportingMultiInstance) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; + mPackageManager = context.getPackageManager(); + mLauncherApps = context.getSystemService(LauncherApps.class); mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; mDisplayController = displayController; @@ -288,8 +301,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator = stageCoordinator; mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); shellInit.addInitCallback(this::onInit, this); - mAppsSupportMultiInstances = mContext.getResources() - .getStringArray(R.array.config_appsSupportMultiInstancesSplit); + mAppsSupportingMultiInstance = appsSupportingMultiInstance; } public SplitScreen asSplitScreen() { @@ -588,7 +600,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (samePackage(packageName, getPackageName(reverseSplitPosition(position)), user.getIdentifier(), getUserId(reverseSplitPosition(position)))) { - if (supportMultiInstancesSplit(packageName)) { + if (supportsMultiInstanceSplit(getShortcutComponent(packageName, shortcutId, user, + mLauncherApps))) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else if (isSplitScreenVisible()) { @@ -609,7 +622,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, activityOptions.toBundle(), user); } - void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, + void startShortcutAndTaskWithLegacyTransition(@NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId) { @@ -621,7 +634,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final int userId1 = shortcutInfo.getUserId(); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(shortcutInfo.getPackage())) { + if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else { @@ -640,7 +653,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, instanceId); } - void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, + void startShortcutAndTask(@NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { @@ -653,7 +666,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final int userId1 = shortcutInfo.getUserId(); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(packageName1)) { + if (supportsMultiInstanceSplit(shortcutInfo.getActivity())) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else { @@ -692,7 +705,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(packageName1)) { + if (supportsMultiInstanceSplit(getComponent(pendingIntent))) { fillInIntent = new Intent(); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); @@ -722,7 +735,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(packageName1)) { + if (supportsMultiInstanceSplit(getComponent(pendingIntent))) { setSecondIntentMultipleTask = true; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else { @@ -757,7 +770,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1); final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2); if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(packageName1)) { + if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) { fillInIntent1 = new Intent(); fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); fillInIntent2 = new Intent(); @@ -794,7 +807,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic(); boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(packageName1)) { + if (supportsMultiInstanceSplit(getComponent(pendingIntent1))) { fillInIntent1 = new Intent(); fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); setSecondIntentMultipleTask = true; @@ -856,7 +869,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return; } if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (supportMultiInstancesSplit(packageName1)) { + if (supportsMultiInstanceSplit(getComponent(intent))) { // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of // the split and there is no reusable background task. fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); @@ -915,16 +928,63 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return taskInfo != null ? taskInfo.userId : -1; } + /** + * Returns whether a specific component desires to be launched in multiple instances for + * split screen. + */ @VisibleForTesting - boolean supportMultiInstancesSplit(String packageName) { - if (packageName != null) { - for (int i = 0; i < mAppsSupportMultiInstances.length; i++) { - if (mAppsSupportMultiInstances[i].equals(packageName)) { - return true; - } + boolean supportsMultiInstanceSplit(@Nullable ComponentName componentName) { + if (componentName == null || componentName.getPackageName() == null) { + // TODO(b/262864589): Handle empty component case + return false; + } + + // Check the pre-defined allow list + final String packageName = componentName.getPackageName(); + for (int i = 0; i < mAppsSupportingMultiInstance.length; i++) { + if (mAppsSupportingMultiInstance[i].equals(packageName)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "application=%s in allowlist supports multi-instance", packageName); + return true; + } + } + + // Check the activity property first + try { + final PackageManager.Property activityProp = mPackageManager.getProperty( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName); + // If the above call doesn't throw a NameNotFoundException, then the activity property + // should override the application property value + if (activityProp.isBoolean()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "activity=%s supports multi-instance", componentName); + return activityProp.getBoolean(); + } else { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Warning: property=%s for activity=%s has non-bool type=%d", + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, + activityProp.getType()); } + } catch (PackageManager.NameNotFoundException nnfe) { + // Not specified in the activity, fall through } + // Check the application property otherwise + try { + final PackageManager.Property appProp = mPackageManager.getProperty( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName); + if (appProp.isBoolean()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "application=%s supports multi-instance", packageName); + return appProp.getBoolean(); + } else { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Warning: property=%s for application=%s has non-bool type=%d", + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, appProp.getType()); + } + } catch (PackageManager.NameNotFoundException nnfe) { + // Not specified in either application or activity + } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 7dec12aac39b..bc1a57572c63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -388,6 +388,7 @@ class SplitScreenTransitions { IBinder startResizeTransition(WindowContainerTransaction wct, Transitions.TransitionHandler handler, + @Nullable TransitionConsumedCallback consumedCallback, @Nullable TransitionFinishedCallback finishCallback) { if (mPendingResize != null) { mPendingResize.cancel(null); @@ -396,13 +397,14 @@ class SplitScreenTransitions { } IBinder transition = mTransitions.startTransition(TRANSIT_CHANGE, wct, handler); - setResizeTransition(transition, finishCallback); + setResizeTransition(transition, consumedCallback, finishCallback); return transition; } void setResizeTransition(@NonNull IBinder transition, + @Nullable TransitionConsumedCallback consumedCallback, @Nullable TransitionFinishedCallback finishCallback) { - mPendingResize = new TransitSession(transition, null /* consumedCb */, finishCallback); + mPendingResize = new TransitSession(transition, consumedCallback, finishCallback); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + " deduced Resize split screen"); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 96e57e71f05c..0781a9e440f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2236,8 +2236,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, sendOnBoundsChanged(); if (ENABLE_SHELL_TRANSITIONS) { mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart"); - mSplitTransitions.startResizeTransition(wct, this, (finishWct, t) -> - mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish")); + mSplitTransitions.startResizeTransition(wct, this, (aborted) -> { + mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed"); + }, (finishWct, t) -> { + mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish"); + }); } else { // Only need screenshot for legacy case because shell transition should screenshot // itself during transition. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index f58aeac918b5..a666e208a1b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -73,6 +73,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.palette.Palette; import com.android.internal.graphics.palette.Quantizer; import com.android.internal.graphics.palette.VariationalKMeansQuantizer; +import com.android.internal.policy.PhoneWindow; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.IconProvider; @@ -245,16 +246,19 @@ public class SplashscreenContentDrawer { } else { windowFlags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; } - params.layoutInDisplayCutoutMode = a.getInt( - R.styleable.Window_windowLayoutInDisplayCutoutMode, - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS); - params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); - a.recycle(); final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo; final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null ? windowInfo.targetActivityInfo : taskInfo.topActivityInfo; + params.layoutInDisplayCutoutMode = a.getInt( + R.styleable.Window_windowLayoutInDisplayCutoutMode, + PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */) + ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + : params.layoutInDisplayCutoutMode); + params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); + a.recycle(); + final int displayId = taskInfo.displayId; // Assumes it's safe to show starting windows of launched apps while // the keyguard is being hidden. This is okay because starting windows never show diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java index 0eb7c2d98e0a..a7843e218a8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -293,11 +293,7 @@ public class ShellController { private class ShellInterfaceImpl implements ShellInterface { @Override public void onInit() { - try { - mMainExecutor.executeBlocking(() -> ShellController.this.handleInit()); - } catch (InterruptedException e) { - throw new RuntimeException("Failed to initialize the Shell in 2s", e); - } + mMainExecutor.execute(ShellController.this::handleInit); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 9f20f49b4094..db845137540b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -21,9 +21,9 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -84,7 +84,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private UnfoldTransitionHandler mUnfoldHandler; private ActivityEmbeddingController mActivityEmbeddingController; - private class MixedTransition { + private static class MixedTransition { static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; /** Both the display and split-state (enter/exit) is changing */ @@ -175,7 +175,6 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, joinFinishArgs(wct); if (mInFlightSubAnimations == 0) { - mActiveTransitions.remove(MixedTransition.this); mFinishCB.onTransitionFinished(mFinishWCT); } } @@ -401,8 +400,12 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, final MixedTransition keyguardMixed = new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition); mActiveTransitions.add(keyguardMixed); - final boolean hasAnimateKeyguard = animateKeyguard(keyguardMixed, info, - startTransaction, finishTransaction, finishCallback); + Transitions.TransitionFinishCallback callback = wct -> { + mActiveTransitions.remove(keyguardMixed); + finishCallback.onTransitionFinished(wct); + }; + final boolean hasAnimateKeyguard = animateKeyguard( + keyguardMixed, info, startTransaction, finishTransaction, callback); if (hasAnimateKeyguard) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Converting mixed transition into a keyguard transition"); @@ -420,27 +423,34 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, if (mixed == null) return false; - if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { - return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction, - finishCallback); - } else if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) { - return animateEnterPipFromActivityEmbedding(mixed, info, startTransaction, - finishTransaction, finishCallback); - } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { + final MixedTransition chosenTransition = mixed; + Transitions.TransitionFinishCallback callback = wct -> { + mActiveTransitions.remove(chosenTransition); + finishCallback.onTransitionFinished(wct); + }; + + if (chosenTransition.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { + return animateEnterPipFromSplit( + chosenTransition, info, startTransaction, finishTransaction, callback); + } else if (chosenTransition.mType + == MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING) { + return animateEnterPipFromActivityEmbedding( + chosenTransition, info, startTransaction, finishTransaction, callback); + } else if (chosenTransition.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { return false; - } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { - final boolean handledToPip = animateOpenIntentWithRemoteAndPip(mixed, info, - startTransaction, finishTransaction, finishCallback); + } else if (chosenTransition.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { + final boolean handledToPip = animateOpenIntentWithRemoteAndPip( + chosenTransition, info, startTransaction, finishTransaction, callback); // Consume the transition on remote handler if the leftover handler already handle this // transition. And if it cannot, the transition will be handled by remote handler, so // don't consume here. // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip - if (handledToPip && mixed.mHasRequestToRemote - && mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) { + if (handledToPip && chosenTransition.mHasRequestToRemote + && chosenTransition.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) { mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null); } return handledToPip; - } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + } else if (chosenTransition.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); // Pip auto-entering info might be appended to recent transition like pressing @@ -449,28 +459,29 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, if (mPipHandler.isEnteringPip(change, info.getType()) && mSplitHandler.getSplitItemPosition(change.getLastParent()) != SPLIT_POSITION_UNDEFINED) { - return animateEnterPipFromSplit(mixed, info, startTransaction, - finishTransaction, finishCallback); + return animateEnterPipFromSplit( + chosenTransition, info, startTransaction, finishTransaction, callback); } } - return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction, - finishCallback); - } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { - return animateKeyguard(mixed, info, startTransaction, finishTransaction, - finishCallback); - } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) { - return animateRecentsDuringKeyguard(mixed, info, startTransaction, finishTransaction, - finishCallback); - } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) { - return animateRecentsDuringDesktop(mixed, info, startTransaction, finishTransaction, - finishCallback); - } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) { - return animateUnfold(mixed, info, startTransaction, finishTransaction, finishCallback); + return animateRecentsDuringSplit( + chosenTransition, info, startTransaction, finishTransaction, callback); + } else if (chosenTransition.mType == MixedTransition.TYPE_KEYGUARD) { + return animateKeyguard( + chosenTransition, info, startTransaction, finishTransaction, callback); + } else if (chosenTransition.mType == MixedTransition.TYPE_RECENTS_DURING_KEYGUARD) { + return animateRecentsDuringKeyguard( + chosenTransition, info, startTransaction, finishTransaction, callback); + } else if (chosenTransition.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) { + return animateRecentsDuringDesktop( + chosenTransition, info, startTransaction, finishTransaction, callback); + } else if (chosenTransition.mType == MixedTransition.TYPE_UNFOLD) { + return animateUnfold( + chosenTransition, info, startTransaction, finishTransaction, callback); } else { - mActiveTransitions.remove(mixed); + mActiveTransitions.remove(chosenTransition); throw new IllegalStateException("Starting mixed animation without a known mixed type? " - + mixed.mType); + + chosenTransition.mType); } } @@ -727,7 +738,11 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, final MixedTransition mixed = new MixedTransition( MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition); mActiveTransitions.add(mixed); - return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback); + Transitions.TransitionFinishCallback callback = wct -> { + mActiveTransitions.remove(mixed); + finishCallback.onTransitionFinished(wct); + }; + return animateEnterPipFromSplit(mixed, info, startT, finishT, callback); } /** 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 cebc4006656a..1a793a16f254 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 @@ -23,13 +23,11 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Handler; -import android.os.IBinder; import android.util.SparseArray; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; -import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -80,16 +78,6 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Override - public void onTransitionReady(IBinder transition, TransitionInfo info, - TransitionInfo.Change change) {} - - @Override - public void onTransitionMerged(IBinder merged, IBinder playing) {} - - @Override - public void onTransitionFinished(IBinder transition) {} - - @Override public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) { mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue); } 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 61a8e9b5dd59..d07c64670bd2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -22,7 +22,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.WindowInsets.Type.statusBars; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -43,7 +42,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; import android.os.Handler; -import android.os.IBinder; import android.os.Looper; import android.util.SparseArray; import android.view.Choreographer; @@ -59,8 +57,6 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; import android.view.ViewConfiguration; -import android.view.WindowManager; -import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -80,8 +76,6 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; -import com.android.wm.shell.recents.RecentsTransitionHandler; -import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -115,7 +109,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; private final Optional<DesktopTasksController> mDesktopTasksController; - private final RecentsTransitionHandler mRecentsTransitionHandler; private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); @@ -154,7 +147,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RecentsTransitionHandler recentsTransitionHandler, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer ) { this( @@ -170,7 +162,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { syncQueue, transitions, desktopTasksController, - recentsTransitionHandler, new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, @@ -191,7 +182,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RecentsTransitionHandler recentsTransitionHandler, DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory, Supplier<SurfaceControl.Transaction> transactionFactory, @@ -207,7 +197,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue = syncQueue; mTransitions = transitions; mDesktopTasksController = desktopTasksController; - mRecentsTransitionHandler = recentsTransitionHandler; mShellCommandHandler = shellCommandHandler; mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; mInputMonitorFactory = inputMonitorFactory; @@ -219,12 +208,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void onInit() { mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener); - mRecentsTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() { - @Override - public void onTransitionStarted(IBinder transition) { - blockRelayoutOnTransitionStarted(transition); - } - }); mShellCommandHandler.addDumpCallback(this::dump, this); mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(), new DesktopModeOnInsetsChangedListener()); @@ -264,48 +247,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } @Override - public void onTransitionReady( - @NonNull IBinder transition, - @NonNull TransitionInfo info, - @NonNull TransitionInfo.Change change) { - if (change.getMode() == WindowManager.TRANSIT_CHANGE - && (info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE - || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE - || info.getType() == Transitions.TRANSIT_MOVE_TO_DESKTOP)) { - mWindowDecorByTaskId.get(change.getTaskInfo().taskId) - .addTransitionPausingRelayout(transition); - } else if (change.getMode() == WindowManager.TRANSIT_TO_BACK - && info.getType() == Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP - && change.getTaskInfo() != null) { - final DesktopModeWindowDecoration decor = - mWindowDecorByTaskId.get(change.getTaskInfo().taskId); - if (decor != null) { - decor.addTransitionPausingRelayout(transition); - } - } else if (change.getMode() == WindowManager.TRANSIT_TO_FRONT - && ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) - && change.getTaskInfo() != null) { - blockRelayoutOnTransitionStarted(transition); - } - } - - @Override - public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { - for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { - final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); - decor.mergeTransitionPausingRelayout(merged, playing); - } - } - - @Override - public void onTransitionFinished(@NonNull IBinder transition) { - for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { - final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); - decor.removeTransitionPausingRelayout(transition); - } - } - - @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (decoration == null) return; @@ -365,16 +306,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } - private void blockRelayoutOnTransitionStarted(IBinder transition) { - // Block relayout on window decorations originating from #onTaskInfoChanges until the - // animation completes to avoid interfering with the transition animation. - for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { - final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); - decor.incrementRelayoutBlock(); - decor.addTransitionPausingRelayout(transition); - } - } - private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, DragDetector.MotionEventHandler { @@ -425,7 +356,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // App sometimes draws before the insets from WindowDecoration#relayout have // been added, so they must be added here mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct); - decoration.incrementRelayoutBlock(); mDesktopTasksController.get().moveToDesktop(decoration, mTaskId, wct); closeOtherSplitTask(mTaskId); } @@ -436,7 +366,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.moveTaskToFullscreen(mTaskId); } else { mDesktopTasksController.ifPresent(c -> - c.moveToFullscreen(mTaskId, mWindowDecorByTaskId.get(mTaskId))); + c.moveToFullscreen(mTaskId)); } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); @@ -604,7 +534,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo, position, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), - newTaskBounds, mWindowDecorByTaskId.get(mTaskId))); + newTaskBounds)); mIsDragging = false; return true; } @@ -812,20 +742,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mContext, mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo, relevantDecor.mTaskSurface); mDesktopTasksController.ifPresent( - c -> { - final int taskId = relevantDecor.mTaskInfo.taskId; - relevantDecor.incrementRelayoutBlock(); - if (isTaskInSplitScreen(taskId)) { - final DesktopModeWindowDecoration otherDecor = - mWindowDecorByTaskId.get( - getOtherSplitTask(taskId).taskId); - if (otherDecor != null) { - otherDecor.incrementRelayoutBlock(); - } - } - c.startDragToDesktop(relevantDecor.mTaskInfo, - mMoveToDesktopAnimator, relevantDecor); - }); + c -> c.startDragToDesktop(relevantDecor.mTaskInfo, + mMoveToDesktopAnimator, relevantDecor)); } } if (mMoveToDesktopAnimator != null) { 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 d08b655e43f8..5f77192fb623 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 @@ -36,7 +36,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; -import android.os.IBinder; import android.util.Log; import android.view.Choreographer; import android.view.MotionEvent; @@ -62,8 +61,6 @@ import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowD import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder; import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder; -import java.util.HashSet; -import java.util.Set; import java.util.function.Supplier; /** @@ -104,8 +101,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private ExclusionRegionListener mExclusionRegionListener; - private final Set<IBinder> mTransitionsPausingRelayout = new HashSet<>(); - private int mRelayoutBlock; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; DesktopModeWindowDecoration( @@ -179,13 +174,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - // TaskListener callbacks and shell transitions aren't synchronized, so starting a shell - // transition can trigger an onTaskInfoChanged call that updates the task's SurfaceControl - // and interferes with the transition animation that is playing at the same time. - if (mRelayoutBlock > 0) { - return; - } - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); // The crop and position of the task should only be set when a task is fluid resizing. In // all other cases, it is expected that the transition handler positions and crops the task @@ -737,16 +725,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return exclusionRegion; } - /** - * If transition exists in mTransitionsPausingRelayout, remove the transition and decrement - * mRelayoutBlock - */ - void removeTransitionPausingRelayout(IBinder transition) { - if (mTransitionsPausingRelayout.remove(transition)) { - mRelayoutBlock--; - } - } - @Override int getCaptionHeightId(@WindowingMode int windowingMode) { return getCaptionHeightIdStatic(windowingMode); @@ -767,35 +745,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return R.id.desktop_mode_caption; } - /** - * Add transition to mTransitionsPausingRelayout - */ - void addTransitionPausingRelayout(IBinder transition) { - mTransitionsPausingRelayout.add(transition); - } - - /** - * If two transitions merge and the merged transition is in mTransitionsPausingRelayout, - * remove the merged transition from the set and add the transition it was merged into. - */ - public void mergeTransitionPausingRelayout(IBinder merged, IBinder playing) { - if (mTransitionsPausingRelayout.remove(merged)) { - mTransitionsPausingRelayout.add(playing); - } - } - - /** - * Increase mRelayoutBlock, stopping relayout if mRelayoutBlock is now greater than 0. - */ - public void incrementRelayoutBlock() { - mRelayoutBlock++; - } - @Override public String toString() { return "{" + "mPositionInParent=" + mPositionInParent + ", " - + "mRelayoutBlock=" + mRelayoutBlock + ", " + "taskId=" + mTaskInfo.taskId + ", " + "windowingMode=" + windowingModeToString(mTaskInfo.getWindowingMode()) + ", " + "isFocused=" + isFocused() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index ae1a3d914be3..01a6012ea314 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -17,9 +17,7 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager; -import android.os.IBinder; import android.view.SurfaceControl; -import android.window.TransitionInfo; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -103,34 +101,4 @@ public interface WindowDecorViewModel { * @param taskInfo the info of the task */ void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); - - /** - * Notifies that a shell transition is about to start. If the transition is of type - * TRANSIT_ENTER_DESKTOP, it will save that transition to unpause relayout for the transitioning - * task after the transition has ended. - * - * @param transition the ready transaction - * @param info of Transition to check if relayout needs to be paused for a task - * @param change a change in the given transition - */ - default void onTransitionReady(IBinder transition, TransitionInfo info, - TransitionInfo.Change change) {} - - /** - * Notifies that a shell transition is about to merge with another to give the window - * decoration a chance to prepare for this merge. - * - * @param merged the transaction being merged - * @param playing the transaction being merged into - */ - default void onTransitionMerged(IBinder merged, IBinder playing) {} - - /** - * Notifies that a shell transition is about to finish to give the window decoration a chance - * to clean up. - * - * @param transaction - */ - default void onTransitionFinished(IBinder transaction) {} - }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt new file mode 100644 index 000000000000..955660c396d0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenUtilsTests.kt @@ -0,0 +1,69 @@ +/* + * 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.common.split + +import android.content.ComponentName +import android.content.pm.LauncherApps +import android.content.pm.ShortcutInfo +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.wm.shell.ShellTestCase +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class SplitScreenUtilsTests : ShellTestCase() { + + @Test + fun getShortcutComponent_nullShortcuts() { + val launcherApps = mock(LauncherApps::class.java).also { + `when`(it.getShortcuts(any(), any())).thenReturn(null) + } + assertEquals(null, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE, + TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps)) + } + + @Test + fun getShortcutComponent_noShortcuts() { + val launcherApps = mock(LauncherApps::class.java).also { + `when`(it.getShortcuts(any(), any())).thenReturn(ArrayList<ShortcutInfo>()) + } + assertEquals(null, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE, + TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps)) + } + + @Test + fun getShortcutComponent_validShortcut() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + val shortcutInfo = ShortcutInfo.Builder(context, "id").setActivity(component).build() + val launcherApps = mock(LauncherApps::class.java).also { + `when`(it.getShortcuts(any(), any())).thenReturn(arrayListOf(shortcutInfo)) + } + assertEquals(component, SplitScreenUtils.getShortcutComponent(TEST_PACKAGE, + TEST_SHORTCUT_ID, UserHandle.CURRENT, launcherApps)) + } + + companion object { + val TEST_PACKAGE = "com.android.wm.shell.common.split" + val TEST_ACTIVITY = "TestActivity"; + val TEST_SHORTCUT_ID = "test_shortcut_1" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 94c862bd7a4f..9249b0a0dfda 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -393,7 +393,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration) + controller.moveToFullscreen(task.taskId) val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) @@ -403,7 +403,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM - controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration) + controller.moveToFullscreen(task.taskId) val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) @@ -411,7 +411,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToFullscreen_nonExistentTask_doesNothing() { - controller.moveToFullscreen(999, desktopModeWindowDecoration) + controller.moveToFullscreen(999) verifyWCTNotExecuted() } @@ -420,7 +420,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) - controller.moveToFullscreen(taskDefaultDisplay.taskId, desktopModeWindowDecoration) + controller.moveToFullscreen(taskDefaultDisplay.taskId) with(getLatestExitDesktopWct()) { assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 855b7ee04702..12a5594ae1da 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; +import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -36,6 +37,8 @@ import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -46,8 +49,10 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import androidx.test.annotation.UiThreadTest; @@ -55,6 +60,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -91,6 +97,10 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class SplitScreenControllerTests extends ShellTestCase { + private static final String TEST_PACKAGE = "com.android.wm.shell.splitscreen"; + private static final String TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.splitscreen.fake"; + private static final String TEST_ACTIVITY = "TestActivity"; + @Mock ShellInit mShellInit; @Mock ShellCommandHandler mShellCommandHandler; @Mock ShellTaskOrganizer mTaskOrganizer; @@ -118,6 +128,8 @@ public class SplitScreenControllerTests extends ShellTestCase { public void setup() { assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)); MockitoAnnotations.initMocks(this); + String[] appsSupportingMultiInstance = mContext.getResources() + .getStringArray(R.array.config_appsSupportMultiInstancesSplit); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, mMainExecutor)); mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, @@ -125,7 +137,8 @@ public class SplitScreenControllerTests extends ShellTestCase { mRootTDAOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, - mDesktopTasksController, mMainExecutor, mStageCoordinator)); + mDesktopTasksController, mMainExecutor, mStageCoordinator, + appsSupportingMultiInstance)); } @Test @@ -200,7 +213,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Test public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() { - doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doReturn(true).when(mSplitScreenController).supportsMultiInstanceSplit(any()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); @@ -237,12 +250,13 @@ public class SplitScreenControllerTests extends ShellTestCase { verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); - verify(mSplitScreenController, never()).supportMultiInstancesSplit(any()); + verify(mSplitScreenController, never()).supportsMultiInstanceSplit(any()); verify(mStageCoordinator, never()).switchSplitPosition(any()); } @Test public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() { + doReturn(true).when(mSplitScreenController).supportsMultiInstanceSplit(any()); doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = @@ -259,14 +273,14 @@ public class SplitScreenControllerTests extends ShellTestCase { mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, SPLIT_POSITION_TOP_OR_LEFT, null); - verify(mSplitScreenController, never()).supportMultiInstancesSplit(any()); + verify(mSplitScreenController, never()).supportsMultiInstanceSplit(any()); verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); } @Test public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() { - doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doReturn(false).when(mSplitScreenController).supportsMultiInstanceSplit(any()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); @@ -283,6 +297,130 @@ public class SplitScreenControllerTests extends ShellTestCase { verify(mStageCoordinator).switchSplitPosition(anyString()); } + @Test + public void supportsMultiInstanceSplit_inStaticAllowList() { + String[] allowList = { TEST_PACKAGE }; + SplitScreenController controller = new SplitScreenController(mContext, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, + mDesktopTasksController, mMainExecutor, mStageCoordinator, + allowList); + ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + assertEquals(true, controller.supportsMultiInstanceSplit(component)); + } + + @Test + public void supportsMultiInstanceSplit_notInStaticAllowList() { + String[] allowList = { TEST_PACKAGE }; + SplitScreenController controller = new SplitScreenController(mContext, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, + mDesktopTasksController, mMainExecutor, mStageCoordinator, + allowList); + ComponentName component = new ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY); + assertEquals(false, controller.supportsMultiInstanceSplit(component)); + } + + @Test + public void supportsMultiInstanceSplit_activityPropertyTrue() + throws PackageManager.NameNotFoundException { + Context context = spy(mContext); + ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + PackageManager pm = mock(PackageManager.class); + doReturn(pm).when(context).getPackageManager(); + PackageManager.Property activityProp = new PackageManager.Property("", true, "", ""); + doReturn(activityProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component)); + PackageManager.Property appProp = new PackageManager.Property("", false, "", ""); + doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.getPackageName())); + + SplitScreenController controller = new SplitScreenController(context, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, + mDesktopTasksController, mMainExecutor, mStageCoordinator, + new String[0]); + // Expect activity property to override application property + assertEquals(true, controller.supportsMultiInstanceSplit(component)); + } + + @Test + public void supportsMultiInstanceSplit_activityPropertyFalseApplicationPropertyTrue() + throws PackageManager.NameNotFoundException { + Context context = spy(mContext); + ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + PackageManager pm = mock(PackageManager.class); + doReturn(pm).when(context).getPackageManager(); + PackageManager.Property activityProp = new PackageManager.Property("", false, "", ""); + doReturn(activityProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component)); + PackageManager.Property appProp = new PackageManager.Property("", true, "", ""); + doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.getPackageName())); + + SplitScreenController controller = new SplitScreenController(context, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, + mDesktopTasksController, mMainExecutor, mStageCoordinator, + new String[0]); + // Expect activity property to override application property + assertEquals(false, controller.supportsMultiInstanceSplit(component)); + } + + @Test + public void supportsMultiInstanceSplit_noActivityPropertyApplicationPropertyTrue() + throws PackageManager.NameNotFoundException { + Context context = spy(mContext); + ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + PackageManager pm = mock(PackageManager.class); + doReturn(pm).when(context).getPackageManager(); + doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty( + eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component)); + PackageManager.Property appProp = new PackageManager.Property("", true, "", ""); + doReturn(appProp).when(pm).getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.getPackageName())); + + SplitScreenController controller = new SplitScreenController(context, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, + mDesktopTasksController, mMainExecutor, mStageCoordinator, + new String[0]); + // Expect fall through to app property + assertEquals(true, controller.supportsMultiInstanceSplit(component)); + } + + @Test + public void supportsMultiInstanceSplit_noActivityOrAppProperty() + throws PackageManager.NameNotFoundException { + Context context = spy(mContext); + ComponentName component = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + PackageManager pm = mock(PackageManager.class); + doReturn(pm).when(context).getPackageManager(); + doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty( + eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component)); + doThrow(PackageManager.NameNotFoundException.class).when(pm).getProperty( + eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), eq(component.getPackageName())); + + SplitScreenController controller = new SplitScreenController(context, mShellInit, + mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, + mRootTDAOrganizer, mDisplayController, mDisplayImeController, + mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, + mIconProvider, mRecentTasks, mLaunchAdjacentController, mWindowDecorViewModel, + mDesktopTasksController, mMainExecutor, mStageCoordinator, + new String[0]); + assertEquals(false, controller.supportsMultiInstanceSplit(component)); + } + private Intent createStartIntent(String activityName) { Intent intent = new Intent(); intent.setComponent(new ComponentName(mContext, activityName)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index e22bf3de30e4..e9da25813510 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -64,6 +64,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Binder; @@ -420,6 +421,30 @@ public class ShellTransitionTests extends ShellTestCase { } @Test + public void testTransitionFilterActivityComponent() { + TransitionFilter filter = new TransitionFilter(); + ComponentName cmpt = new ComponentName("testpak", "testcls"); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mTopActivity = cmpt; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + final RunningTaskInfo taskInf = createTaskInfo(1); + final TransitionInfo openTask = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, taskInf).build(); + assertFalse(filter.matches(openTask)); + + taskInf.topActivity = cmpt; + final TransitionInfo openTaskCmpt = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, taskInf).build(); + assertTrue(filter.matches(openTaskCmpt)); + + final TransitionInfo openAct = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, cmpt).build(); + assertTrue(filter.matches(openAct)); + } + + @Test public void testRegisteredRemoteTransition() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java index 834385832e4a..b8939e6ff623 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java @@ -21,6 +21,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static org.mockito.Mockito.mock; import android.app.ActivityManager; +import android.content.ComponentName; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -50,20 +51,34 @@ public class TransitionInfoBuilder { } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, - @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) { + @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo, + ComponentName activityComponent) { final TransitionInfo.Change change = new TransitionInfo.Change( taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */)); change.setMode(mode); change.setFlags(flags); change.setTaskInfo(taskInfo); + change.setActivityComponent(activityComponent); return addChange(change); } + /** Add a change to the TransitionInfo */ + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) { + return addChange(mode, flags, taskInfo, null /* activityComponent */); + } + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, ActivityManager.RunningTaskInfo taskInfo) { return addChange(mode, TransitionInfo.FLAG_NONE, taskInfo); } + /** Add a change to the TransitionInfo */ + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + ComponentName activityComponent) { + return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent); + } + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 883c24e78076..f84685a92b57 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -27,7 +27,6 @@ import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.os.Handler -import android.os.IBinder import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.Choreographer @@ -40,8 +39,6 @@ import android.view.SurfaceControl import android.view.SurfaceView import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars -import android.view.WindowManager -import android.window.TransitionInfo import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer @@ -53,8 +50,6 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTasksController -import com.android.wm.shell.recents.RecentsTransitionHandler -import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.sysui.KeyguardChangeListener import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -100,7 +95,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockShellController: ShellController @Mock private lateinit var mockShellExecutor: ShellExecutor @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock private lateinit var mockRecentsTransitionHandler: RecentsTransitionHandler @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler private val transactionFactory = Supplier<SurfaceControl.Transaction> { @@ -127,7 +121,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockSyncQueue, mockTransitions, Optional.of(mockDesktopTasksController), - mockRecentsTransitionHandler, mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, @@ -275,48 +268,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testRelayoutBlockedDuringRecentsTransition() { - val recentsCaptor = argumentCaptor<RecentsTransitionStateListener>() - verify(mockRecentsTransitionHandler).addTransitionStateListener(recentsCaptor.capture()) - - val transition = mock(IBinder::class.java) - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) - val decoration = setUpMockDecorationForTask(task) - - // Make sure a window decorations exists first by launching a freeform task. - onTaskOpening(task) - // Now call back when a Recents transition starts. - recentsCaptor.firstValue.onTransitionStarted(transition) - - verify(decoration).incrementRelayoutBlock() - verify(decoration).addTransitionPausingRelayout(transition) - } - - @Test - fun testRelayoutBlockedDuringKeyguardTransition() { - val transition = mock(IBinder::class.java) - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) - val decoration = setUpMockDecorationForTask(task) - val transitionInfo = mock(TransitionInfo::class.java) - val transitionChange = mock(TransitionInfo.Change::class.java) - val taskInfo = mock(RunningTaskInfo()::class.java) - - // Replicate a keyguard going away transition for a task - whenever(transitionInfo.getFlags()) - .thenReturn(WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY) - whenever(transitionChange.getMode()).thenReturn(WindowManager.TRANSIT_TO_FRONT) - whenever(transitionChange.getTaskInfo()).thenReturn(taskInfo) - - // Make sure a window decorations exists first by launching a freeform task. - onTaskOpening(task) - // OnTransition ready is called when a keyguard going away transition happens - desktopModeWindowDecorViewModel - .onTransitionReady(transition, transitionInfo, transitionChange) - - verify(decoration).incrementRelayoutBlock() - verify(decoration).addTransitionPausingRelayout(transition) - } - @Test fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) val decoration = setUpMockDecorationForTask(task) diff --git a/location/api/current.txt b/location/api/current.txt index 0c23d8cd77e0..c55676bc1e78 100644 --- a/location/api/current.txt +++ b/location/api/current.txt @@ -414,7 +414,7 @@ package android.location { field public static final int TYPE_GPS_L5CNAV = 259; // 0x103 field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L1 = 1795; // 0x703 field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L5 = 1794; // 0x702 - field @Deprecated public static final int TYPE_IRN_L5CA = 1793; // 0x701 + field public static final int TYPE_IRN_L5CA = 1793; // 0x701 field public static final int TYPE_QZS_L1CA = 1025; // 0x401 field public static final int TYPE_SBS = 513; // 0x201 field public static final int TYPE_UNKNOWN = 0; // 0x0 diff --git a/location/java/android/location/GnssNavigationMessage.java b/location/java/android/location/GnssNavigationMessage.java index 5e3f8033d116..7a667ae4ef6c 100644 --- a/location/java/android/location/GnssNavigationMessage.java +++ b/location/java/android/location/GnssNavigationMessage.java @@ -78,9 +78,7 @@ public final class GnssNavigationMessage implements Parcelable { public static final int TYPE_GAL_F = 0x0602; /** * NavIC L5 C/A message contained in the structure. - * @deprecated deprecated. */ - @Deprecated public static final int TYPE_IRN_L5CA = 0x0701; /** NavIC L5 message contained in the structure. */ @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index a5a69f987113..4918289e8b5c 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -21,6 +21,7 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; +import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; @@ -10081,6 +10082,28 @@ public class AudioManager { } } + /** + * @hide + * Checks whether a notification sound should be played or not, as reported by the state + * of the audio framework. Querying whether playback should proceed is favored over + * playing and letting the sound be muted or not. + * @param aa the {@link AudioAttributes} of the notification about to maybe play + * @return true if the audio framework state is such that the notification should be played + * because at time of checking, and the notification will be heard, + * false otherwise + */ + @TestApi + @FlaggedApi(FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING) + @RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE) + public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) { + final IAudioService service = getService(); + try { + return service.shouldNotificationSoundPlay(Objects.requireNonNull(aa)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + //==================================================================== // Mute await connection diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 5c268d4ab652..2eec9b3d4a09 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -775,4 +775,8 @@ interface IAudioService { @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss(); + + @EnforcePermission("QUERY_AUDIO_STATE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)") + boolean shouldNotificationSoundPlay(in AudioAttributes aa); } diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 0eabe66e9a69..838630fcccb9 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -943,6 +943,10 @@ public final class MediaRoute2Info implements Parcelable { .append(getId()) .append(", name=") .append(getName()) + .append(", type=") + .append(getDeviceTypeString(getType())) + .append(", isSystem=") + .append(isSystemRoute()) .append(", features=") .append(getFeatures()) .append(", iconUri=") diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java index 60497fe22dcf..47637b82111a 100644 --- a/media/java/android/media/session/PlaybackState.java +++ b/media/java/android/media/session/PlaybackState.java @@ -15,7 +15,10 @@ */ package android.media.session; +import static com.android.media.flags.Flags.FLAG_ENABLE_NOTIFYING_ACTIVITY_MANAGER_WITH_MEDIA_SESSION_STATUS_CHANGE; + import android.annotation.DrawableRes; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.LongDef; import android.annotation.Nullable; @@ -189,7 +192,8 @@ public final class PlaybackState implements Parcelable { */ @IntDef({STATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING, STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING, - STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM}) + STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM, + STATE_PLAYBACK_SUPPRESSED}) @Retention(RetentionPolicy.SOURCE) public @interface State {} @@ -286,6 +290,19 @@ public final class PlaybackState implements Parcelable { public static final int STATE_SKIPPING_TO_QUEUE_ITEM = 11; /** + * State indicating that playback is paused due to an external transient interruption, like a + * phone call. + * + * <p>This state is different from {@link #STATE_PAUSED} in that it is deemed transitory, + * possibly allowing the service associated to the session in this state to run in the + * foreground. + * + * @see Builder#setState + */ + @FlaggedApi(FLAG_ENABLE_NOTIFYING_ACTIVITY_MANAGER_WITH_MEDIA_SESSION_STATUS_CHANGE) + public static final int STATE_PLAYBACK_SUPPRESSED = 12; + + /** * Use this value for the position to indicate the position is not known. */ public static final long PLAYBACK_POSITION_UNKNOWN = -1; @@ -384,6 +401,7 @@ public final class PlaybackState implements Parcelable { * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li> + * <li> {@link PlaybackState#STATE_PLAYBACK_SUPPRESSED}</li> * </ul> */ @State @@ -507,6 +525,7 @@ public final class PlaybackState implements Parcelable { * <li>{@link #STATE_SKIPPING_TO_NEXT}</li> * <li>{@link #STATE_SKIPPING_TO_PREVIOUS}</li> * <li>{@link #STATE_SKIPPING_TO_QUEUE_ITEM}</li> + * <li>{@link #STATE_PLAYBACK_SUPPRESSED}</li> * </ul> */ public boolean isActive() { @@ -519,33 +538,12 @@ public final class PlaybackState implements Parcelable { case PlaybackState.STATE_BUFFERING: case PlaybackState.STATE_CONNECTING: case PlaybackState.STATE_PLAYING: + case PlaybackState.STATE_PLAYBACK_SUPPRESSED: return true; } return false; } - /** - * Returns whether the service holding the media session should run in the foreground when the - * media session has this playback state or not. - * - * @hide - */ - public boolean shouldAllowServiceToRunInForeground() { - switch (mState) { - case PlaybackState.STATE_PLAYING: - case PlaybackState.STATE_FAST_FORWARDING: - case PlaybackState.STATE_REWINDING: - case PlaybackState.STATE_BUFFERING: - case PlaybackState.STATE_CONNECTING: - case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: - case PlaybackState.STATE_SKIPPING_TO_NEXT: - case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM: - return true; - default: - return false; - } - } - public static final @android.annotation.NonNull Parcelable.Creator<PlaybackState> CREATOR = new Parcelable.Creator<PlaybackState>() { @Override @@ -586,6 +584,8 @@ public final class PlaybackState implements Parcelable { return "SKIPPING_TO_NEXT"; case STATE_SKIPPING_TO_QUEUE_ITEM: return "SKIPPING_TO_QUEUE_ITEM"; + case STATE_PLAYBACK_SUPPRESSED: + return "STATE_PLAYBACK_SUPPRESSED"; default: return "UNKNOWN"; } @@ -823,6 +823,7 @@ public final class PlaybackState implements Parcelable { * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li> + * <li> {@link PlaybackState#STATE_PLAYBACK_SUPPRESSED}</li> * </ul> * * @param state The current state of playback. @@ -867,6 +868,7 @@ public final class PlaybackState implements Parcelable { * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li> + * <li> {@link PlaybackState#STATE_PLAYBACK_SUPPRESSED}</li> * </ul> * * @param state The current state of playback. diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 8ac364e72fef..b2c23a401117 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -69,7 +69,8 @@ class CredentialAutofillService : AutofillService() { companion object { private const val TAG = "CredAutofill" - private const val SESSION_ID_KEY = "session_id" + private const val SESSION_ID_KEY = "autofill_session_id" + private const val REQUEST_ID_KEY = "autofill_request_id" private const val CRED_HINT_PREFIX = "credential=" private const val REQUEST_DATA_KEY = "requestData" private const val CANDIDATE_DATA_KEY = "candidateQueryData" @@ -97,16 +98,23 @@ class CredentialAutofillService : AutofillService() { val callingPackage = structure.activityComponent.packageName Log.i(TAG, "onFillCredentialRequest called for $callingPackage") - var sessionId = request.clientState?.getInt(SESSION_ID_KEY) - - Log.i(TAG, "Autofill sessionId: " + sessionId) - if (sessionId == null) { - Log.i(TAG, "Session Id not found") - callback.onFailure("Session Id not found") + val clientState = request.clientState + if (clientState == null) { + Log.i(TAG, "Client state not found") + callback.onFailure("Client state not found") + return + } + val sessionId = clientState.getInt(SESSION_ID_KEY) + val requestId = clientState.getInt(REQUEST_ID_KEY) + Log.i(TAG, "Autofill sessionId: $sessionId, autofill requestId: $requestId") + if (sessionId == 0 || requestId == 0) { + Log.i(TAG, "Session Id or request Id not found") + callback.onFailure("Session Id or request Id not found") return } - val getCredRequest: GetCredentialRequest? = getCredManRequest(structure) + val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId, + requestId) if (getCredRequest == null) { Log.i(TAG, "No credential manager request found") callback.onFailure("No credential manager request found") @@ -515,12 +523,19 @@ class CredentialAutofillService : AutofillService() { TODO("Not yet implemented") } - private fun getCredManRequest(structure: AssistStructure): GetCredentialRequest? { + private fun getCredManRequest( + structure: AssistStructure, + sessionId: Int, + requestId: Int + ): GetCredentialRequest? { val credentialOptions: MutableList<CredentialOption> = mutableListOf() traverseStructure(structure, credentialOptions) if (credentialOptions.isNotEmpty()) { - return GetCredentialRequest.Builder(Bundle.EMPTY) + val dataBundle = Bundle() + dataBundle.putInt(SESSION_ID_KEY, sessionId) + dataBundle.putInt(REQUEST_ID_KEY, requestId) + return GetCredentialRequest.Builder(dataBundle) .setCredentialOptions(credentialOptions) .build() } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java index 170cb4546d0c..9ad3e3c0af0f 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java @@ -91,7 +91,8 @@ public class UninstallerActivity extends Activity { // be stale, if e.g. the app was uninstalled while the activity was destroyed. super.onCreate(null); - if (usePiaV2() && !isTv()) { + // TODO(b/318521110) Enable PIA v2 for archive dialog. + if (usePiaV2() && !isTv() && !isArchiveDialog(getIntent())) { Log.i(TAG, "Using Pia V2"); boolean returnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false); @@ -224,6 +225,11 @@ public class UninstallerActivity extends Activity { showConfirmationDialog(); } + private boolean isArchiveDialog(Intent intent) { + return (intent.getIntExtra(PackageInstaller.EXTRA_DELETE_FLAGS, 0) + & PackageManager.DELETE_ARCHIVE) != 0; + } + /** * Parses specific {@link android.content.pm.PackageManager.DeleteFlags} from {@link Intent} * to archive an app if requested. diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 326e533df0d8..aeabbd53d177 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -46,6 +46,7 @@ import com.android.packageinstaller.common.InstallEventReceiver import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_DONE import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_INTERNAL_ERROR import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_POLICY +import com.android.packageinstaller.v2.model.InstallAborted.Companion.DLG_NONE import com.android.packageinstaller.v2.model.InstallAborted.Companion.DLG_PACKAGE_ERROR import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_ANONYMOUS_SOURCE import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_INSTALL_CONFIRMATION @@ -283,14 +284,15 @@ class InstallRepository(private val context: Context) { createSessionParams(intent, pfd, uri.toString()) stagedSessionId = packageInstaller.createSession(params) } - } catch (e: IOException) { + } catch (e: Exception) { Log.w(LOG_TAG, "Failed to create a staging session", e) _stagingResult.value = InstallAborted( ABORT_REASON_INTERNAL_ERROR, resultIntent = Intent().putExtra( Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK ), - activityResultCode = Activity.RESULT_FIRST_USER + activityResultCode = Activity.RESULT_FIRST_USER, + errorDialogType = if (e is IOException) DLG_PACKAGE_ERROR else DLG_NONE ) return } @@ -313,6 +315,14 @@ class InstallRepository(private val context: Context) { ) } } + } else { + _stagingResult.value = InstallAborted( + ABORT_REASON_INTERNAL_ERROR, + resultIntent = Intent().putExtra( + Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_URI + ), + activityResultCode = Activity.RESULT_FIRST_USER + ) } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt index be49b39e9a48..bbb9bca6db51 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -122,13 +122,14 @@ data class InstallAborted( */ val resultIntent: Intent? = null, val activityResultCode: Int = Activity.RESULT_CANCELED, - val errorDialogType: Int? = 0, + val errorDialogType: Int? = DLG_NONE, ) : InstallStage(STAGE_ABORTED) { companion object { const val ABORT_REASON_INTERNAL_ERROR = 0 const val ABORT_REASON_POLICY = 1 const val ABORT_REASON_DONE = 2 + const val DLG_NONE = 0 const val DLG_PACKAGE_ERROR = 1 } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt index c109fc673ec4..1d4d1786c761 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt @@ -34,6 +34,8 @@ interface InstallActionListener { */ fun onNegativeResponse(stageCode: Int) + fun onNegativeResponse(resultCode: Int, data: Intent?) + /** * Launch the intent to open the newly installed / updated app. */ diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt index 2b610d7b06f5..6f8eca3655b5 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -36,10 +36,10 @@ import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import com.android.packageinstaller.R -import com.android.packageinstaller.v2.model.InstallRepository import com.android.packageinstaller.v2.model.InstallAborted import com.android.packageinstaller.v2.model.InstallFailed import com.android.packageinstaller.v2.model.InstallInstalling +import com.android.packageinstaller.v2.model.InstallRepository import com.android.packageinstaller.v2.model.InstallStage import com.android.packageinstaller.v2.model.InstallSuccess import com.android.packageinstaller.v2.model.InstallUserActionRequired @@ -50,6 +50,7 @@ import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment +import com.android.packageinstaller.v2.ui.fragments.ParseErrorFragment import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment import com.android.packageinstaller.v2.viewmodel.InstallViewModel import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory @@ -124,8 +125,15 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { InstallStage.STAGE_ABORTED -> { val aborted = installStage as InstallAborted when (aborted.abortReason) { - InstallAborted.ABORT_REASON_DONE, InstallAborted.ABORT_REASON_INTERNAL_ERROR -> - setResult(aborted.activityResultCode, aborted.resultIntent, true) + InstallAborted.ABORT_REASON_DONE, + InstallAborted.ABORT_REASON_INTERNAL_ERROR -> { + if (aborted.errorDialogType == InstallAborted.DLG_PACKAGE_ERROR) { + val parseErrorDialog = ParseErrorFragment(aborted) + showDialogInner(parseErrorDialog) + } else { + setResult(aborted.activityResultCode, aborted.resultIntent, true) + } + } InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted) else -> setResult(Activity.RESULT_CANCELED, null, true) @@ -204,7 +212,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction) // Don't finish the package installer app since the next dialog // will be shown by this app - shouldFinish = blockedByPolicyDialog != null + shouldFinish = blockedByPolicyDialog == null showDialogInner(blockedByPolicyDialog) } setResult(Activity.RESULT_CANCELED, null, shouldFinish) @@ -267,6 +275,10 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { setResult(Activity.RESULT_CANCELED, null, true) } + override fun onNegativeResponse(resultCode: Int, data: Intent?) { + setResult(resultCode, data, true) + } + override fun sendUnknownAppsIntent(sourcePackageName: String) { val settingsIntent = Intent() settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java new file mode 100644 index 000000000000..68d48d62a0b6 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ParseErrorFragment.java @@ -0,0 +1,64 @@ +/* + * 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.packageinstaller.v2.ui.fragments; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import com.android.packageinstaller.R; +import com.android.packageinstaller.v2.model.InstallAborted; +import com.android.packageinstaller.v2.ui.InstallActionListener; + +public class ParseErrorFragment extends DialogFragment { + + private static final String TAG = ParseErrorFragment.class.getSimpleName(); + private final InstallAborted mDialogData; + private InstallActionListener mInstallActionListener; + + public ParseErrorFragment(InstallAborted dialogData) { + mDialogData = dialogData; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mInstallActionListener = (InstallActionListener) context; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setMessage(R.string.Parse_error_dlg_text) + .setPositiveButton(R.string.ok, + (dialog, which) -> + mInstallActionListener.onNegativeResponse( + mDialogData.getActivityResultCode(), mDialogData.getResultIntent())) + .create(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + mInstallActionListener.onNegativeResponse( + mDialogData.getActivityResultCode(), mDialogData.getResultIntent()); + } +} diff --git a/packages/SettingsLib/LintChecker/Android.bp b/packages/SettingsLib/LintChecker/Android.bp new file mode 100644 index 000000000000..eb489b1de380 --- /dev/null +++ b/packages/SettingsLib/LintChecker/Android.bp @@ -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 { + // 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"], +} + +java_library_host { + name: "SettingsLibLintChecker", + srcs: ["src/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/NullabilityAnnotationsDetector.kt b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/NullabilityAnnotationsDetector.kt new file mode 100644 index 000000000000..1f062619b261 --- /dev/null +++ b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/NullabilityAnnotationsDetector.kt @@ -0,0 +1,146 @@ +/* + * 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.settingslib.tools.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiPrimitiveType +import com.intellij.psi.PsiType +import org.jetbrains.uast.UAnnotated +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +class NullabilityAnnotationsDetector : Detector(), Detector.UastScanner { + override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler? { + if (!context.isJavaFile()) return null + + return object : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (node.isPublic() && node.name != ANONYMOUS_CONSTRUCTOR) { + node.verifyMethod() + node.verifyMethodParameters() + } + } + + private fun UMethod.isPublic() = modifierList.hasModifierProperty(PsiModifier.PUBLIC) + + private fun UMethod.verifyMethod() { + if (isConstructor) return + if (returnType.isPrimitive()) return + checkAnnotation(METHOD_MSG) + } + + private fun UMethod.verifyMethodParameters() { + for (parameter in uastParameters) { + if (parameter.type.isPrimitive()) continue + parameter.checkAnnotation(PARAMETER_MSG) + } + } + + private fun PsiType?.isPrimitive() = this is PsiPrimitiveType + + private fun UAnnotated.checkAnnotation(message: String) { + val oldAnnotation = findOldNullabilityAnnotation() + val oldAnnotationName = oldAnnotation?.qualifiedName?.substringAfterLast('.') + + if (oldAnnotationName != null) { + val annotation = "androidx.annotation.$oldAnnotationName" + reportIssue( + REQUIRE_NULLABILITY_ISSUE, + "Prefer $annotation", + LintFix.create() + .replace() + .range(context.getLocation(oldAnnotation)) + .with("@$annotation") + .autoFix() + .build() + ) + } else if (!hasNullabilityAnnotation()) { + reportIssue(REQUIRE_NULLABILITY_ISSUE, message) + } + } + + private fun UElement.reportIssue( + issue: Issue, + message: String, + quickfixData: LintFix? = null, + ) { + context.report( + issue = issue, + scope = this, + location = context.getNameLocation(this), + message = message, + quickfixData = quickfixData, + ) + } + + private fun UAnnotated.findOldNullabilityAnnotation() = + uAnnotations.find { it.qualifiedName in oldAnnotations } + + private fun UAnnotated.hasNullabilityAnnotation() = + uAnnotations.any { it.qualifiedName in validAnnotations } + } + } + + private fun JavaContext.isJavaFile() = psiFile?.fileElementType.toString().startsWith("java") + + companion object { + private val validAnnotations = arrayOf("androidx.annotation.NonNull", + "androidx.annotation.Nullable") + + private val oldAnnotations = arrayOf("android.annotation.NonNull", + "android.annotation.Nullable", + ) + + private const val ANONYMOUS_CONSTRUCTOR = "<anon-init>" + + private const val METHOD_MSG = + "Java public method return with non-primitive type must add androidx annotation. " + + "Example: @NonNull | @Nullable Object functionName() {}" + + private const val PARAMETER_MSG = + "Java public method parameter with non-primitive type must add androidx " + + "annotation. Example: functionName(@NonNull Context context, " + + "@Nullable Object obj) {}" + + internal val REQUIRE_NULLABILITY_ISSUE = Issue + .create( + id = "RequiresNullabilityAnnotation", + briefDescription = "Requires nullability annotation for function", + explanation = "All public java APIs should specify nullability annotations for " + + "methods and parameters.", + category = Category.CUSTOM_LINT_CHECKS, + priority = 3, + severity = Severity.WARNING, + androidSpecific = true, + implementation = Implementation( + NullabilityAnnotationsDetector::class.java, + Scope.JAVA_FILE_SCOPE, + ), + ) + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/SettingsLintIssueRegistry.kt b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/SettingsLintIssueRegistry.kt new file mode 100644 index 000000000000..e0ab24afee5a --- /dev/null +++ b/packages/SettingsLib/LintChecker/src/com/android/settingslib/tools/lint/SettingsLintIssueRegistry.kt @@ -0,0 +1,28 @@ +/* + * 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.settingslib.tools.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.detector.api.CURRENT_API +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +class SettingsLintIssueRegistry : IssueRegistry() { + override val issues = listOf(NullabilityAnnotationsDetector.REQUIRE_NULLABILITY_ISSUE) + + override val api: Int = CURRENT_API +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index c143390f269c..b7f2c1e583f6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -34,6 +34,15 @@ object SettingsDimension { end = itemPaddingEnd, bottom = itemPaddingVertical, ) + val textFieldPadding = PaddingValues( + start = itemPaddingStart, + end = itemPaddingEnd, + ) + val menuFieldPadding = PaddingValues( + start = itemPaddingStart, + end = itemPaddingEnd, + bottom = itemPaddingVertical, + ) val itemPaddingAround = 8.dp val itemDividerHeight = 32.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt index 0d6c064998ae..f6692a356899 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt @@ -51,7 +51,7 @@ fun SettingsExposedDropdownMenuBox( onExpandedChange = { expanded = it }, modifier = Modifier .width(350.dp) - .padding(SettingsDimension.itemPadding), + .padding(SettingsDimension.menuFieldPadding), ) { OutlinedTextField( // The `menuAnchor` modifier must be passed to the text field for correctness. diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt index 5d248e192c7a..ba8e354fa0c6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt @@ -63,7 +63,7 @@ fun SettingsExposedDropdownMenuCheckBox( onExpandedChange = { expanded = it }, modifier = Modifier .width(350.dp) - .padding(SettingsDimension.itemPadding) + .padding(SettingsDimension.menuFieldPadding) .onSizeChanged { dropDownWidth = it.width }, ) { OutlinedTextField( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt index e0dd4e17ce38..2ce3c66796db 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt @@ -42,7 +42,7 @@ fun SettingsOutlinedTextField( OutlinedTextField( modifier = Modifier .fillMaxWidth() - .padding(SettingsDimension.itemPadding), + .padding(SettingsDimension.textFieldPadding), value = value, onValueChange = onTextChange, label = { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt index 0757df347d68..3102a00a24fd 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsTextFieldPassword.kt @@ -52,7 +52,7 @@ fun SettingsTextFieldPassword( var visibility by remember { mutableStateOf(false) } OutlinedTextField( modifier = Modifier - .padding(SettingsDimension.itemPadding) + .padding(SettingsDimension.menuFieldPadding) .fillMaxWidth(), value = value, onValueChange = onTextChange, diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index ebcca42bb588..59254925dbcf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -184,10 +184,6 @@ public class LocalMediaManager implements BluetoothCallback { return false; } - if (mCurrentConnectedDevice != null) { - mCurrentConnectedDevice.disconnect(); - } - device.setState(MediaDeviceState.STATE_CONNECTING); mInfoMediaManager.connectToDevice(device); return true; diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index f2d9d1493c74..0c4cf769ca90 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -396,12 +396,6 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { } /** - * Stop transfer MediaDevice - */ - public void disconnect() { - } - - /** * Set current device's state */ public void setState(@LocalMediaManager.MediaDeviceState int state) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java index 999e8d508e19..9a7d4f1540df 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java @@ -147,7 +147,6 @@ public class LocalMediaManagerTest { mLocalMediaManager.registerCallback(mCallback); assertThat(mLocalMediaManager.connectDevice(device)).isTrue(); - verify(currentDevice).disconnect(); verify(mInfoMediaManager).connectToDevice(device); } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 2e39adc8f79e..add313419c7d 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -93,6 +93,7 @@ public class GlobalSettings { Settings.Global.Wearable.CLOCKWORK_AUTO_TIME, Settings.Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE, Settings.Global.Wearable.CLOCKWORK_24HR_TIME, + Settings.Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED, Settings.Global.Wearable.MUTE_WHEN_OFF_BODY_ENABLED, Settings.Global.Wearable.AMBIENT_ENABLED, Settings.Global.Wearable.AMBIENT_TILT_TO_WAKE, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 502239513002..c0a076095434 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -450,6 +450,8 @@ public class GlobalSettingsValidators { VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put( + Global.Wearable.CONSISTENT_NOTIFICATION_BLOCKING_ENABLED, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Global.FORCE_ENABLE_PSS_PROFILING, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java index bd99a8bbb09f..74fd828f97ea 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java @@ -99,7 +99,6 @@ final class WritableNamespacePrefixes { "kiwi", "latency_tracker", "launcher", - "launcher_lily", "leaked_animator", "lmkd_native", "location", diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 477c42e01fba..507d9c467d68 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -810,6 +810,9 @@ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT" /> <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <!-- Permissions required for CTS test - CtsAppFgsTestCases --> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 42107b7a3182..d3a89f447d1f 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -157,7 +157,7 @@ android_library { "SystemUI-res", "WifiTrackerLib", "WindowManager-Shell", - "SystemUIAnimationLib", + "PlatformAnimationLib", "SystemUICommon", "SystemUICustomizationLib", "SystemUILogLib", @@ -274,7 +274,7 @@ android_library { static_libs: [ "SystemUI-res", "WifiTrackerLib", - "SystemUIAnimationLib", + "PlatformAnimationLib", "SystemUIPluginLib", "SystemUISharedLib", "SystemUICustomizationLib", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index a03fa9b39bfc..7443e4ccf79e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -84,6 +84,7 @@ <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/> <uses-permission android:name="android.permission.LOCATION_HARDWARE" /> <uses-permission android:name="android.permission.NETWORK_FACTORY" /> + <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" /> <!-- Physical hardware --> <uses-permission android:name="android.permission.MANAGE_USB" /> <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" /> diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig index 1ad16667f317..d0e6b2865891 100644 --- a/packages/SystemUI/aconfig/predictive_back.aconfig +++ b/packages/SystemUI/aconfig/predictive_back.aconfig @@ -19,4 +19,11 @@ flag { namespace: "systemui" description: "Enable Predictive Back Animation in Bouncer" bug: "309545085" +} + +flag { + name: "predictive_back_animate_dialogs" + namespace: "systemui" + description: "Enable Predictive Back Animation for SysUI dialogs" + bug: "309545085" }
\ No newline at end of file diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp index 8438051e3430..872187abb9db 100644 --- a/packages/SystemUI/animation/Android.bp +++ b/packages/SystemUI/animation/Android.bp @@ -23,7 +23,7 @@ package { android_library { - name: "SystemUIAnimationLib", + name: "PlatformAnimationLib", use_resource_processor: true, srcs: [ diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index b738e2bc972b..efdbfdb83c70 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -494,7 +494,7 @@ constructor( } for (i in 0 until drawable.numberOfLayers) { - (drawable.getDrawable(i) as? GradientDrawable)?.cornerRadii = radii + applyBackgroundRadii(drawable.getDrawable(i), radii) } } diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp index 42d088f218a1..9a4347d2afe4 100644 --- a/packages/SystemUI/compose/core/Android.bp +++ b/packages/SystemUI/compose/core/Android.bp @@ -30,7 +30,7 @@ android_library { ], static_libs: [ - "SystemUIAnimationLib", + "PlatformAnimationLib", "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", 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 8bd5ddb060c3..d76f0ff3ec18 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,13 +20,13 @@ import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout -import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -45,6 +45,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.TouchApp +import androidx.compose.material.icons.outlined.Widgets import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults @@ -52,6 +54,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -72,11 +75,14 @@ import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize 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 com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize @@ -94,13 +100,16 @@ fun CommunalHub( onEditDone: (() -> Unit)? = null, ) { val communalContent by viewModel.communalContent.collectAsState(initial = emptyList()) + val isPopupOnDismissCtaShowing by + viewModel.isPopupOnDismissCtaShowing.collectAsState(initial = false) var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var toolbarSize: IntSize? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var isDraggingToRemove by remember { mutableStateOf(false) } Box( - modifier = modifier.fillMaxSize().background(Color.White), + modifier = + modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant), ) { CommunalHubLazyGrid( communalContent = communalContent, @@ -111,7 +120,8 @@ fun CommunalHub( isDraggingToRemove = checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates) isDraggingToRemove - } + }, + onOpenWidgetPicker = onOpenWidgetPicker, ) if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) { @@ -128,6 +138,10 @@ fun CommunalHub( } } + if (isPopupOnDismissCtaShowing) { + PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta) + } + // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge // swipe back to the blank scene. @@ -148,13 +162,14 @@ private fun BoxScope.CommunalHubLazyGrid( contentPadding: PaddingValues, 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(communalContent, viewModel) + val contentListState = rememberContentListState(list, viewModel) list = contentListState.list // for drag & drop operations within the communal hub grid dragDropState = @@ -166,7 +181,7 @@ private fun BoxScope.CommunalHubLazyGrid( gridModifier = gridModifier .fillMaxSize() - .dragContainer(dragDropState, beforeContentPadding(contentPadding)) + .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel) .onGloballyPositioned { setGridCoordinates(it) } // for widgets dropped from other activities val dragAndDropTargetState = @@ -207,17 +222,16 @@ private fun BoxScope.CommunalHubLazyGrid( if (viewModel.isEditMode && dragDropState != null) { DraggableItem( dragDropState = dragDropState, - enabled = true, + enabled = list[index] is CommunalContentModel.Widget, index = index, size = size - ) { isDragging -> - val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp) + ) { _ -> CommunalContent( modifier = cardModifier, - elevation = elevation, model = list[index], viewModel = viewModel, size = size, + onOpenWidgetPicker = onOpenWidgetPicker, ) } } else { @@ -258,16 +272,11 @@ private fun Toolbar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - val buttonContentPadding = - PaddingValues( - vertical = Dimensions.ToolbarButtonPaddingVertical, - horizontal = Dimensions.ToolbarButtonPaddingHorizontal, - ) val spacerModifier = Modifier.width(Dimensions.ToolbarButtonSpaceBetween) Button( onClick = onOpenWidgetPicker, - colors = filledSecondaryButtonColors(), - contentPadding = buttonContentPadding + colors = filledButtonColors(), + contentPadding = Dimensions.ButtonPadding ) { Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor)) Spacer(spacerModifier) @@ -276,25 +285,40 @@ private fun Toolbar( ) } - val buttonColors = - if (isDraggingToRemove) filledButtonColors() else ButtonDefaults.outlinedButtonColors() - OutlinedButton( - onClick = {}, - colors = buttonColors, - contentPadding = buttonContentPadding, - modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }, - ) { - Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_open_widget_editor)) - Spacer(spacerModifier) - Text( - text = stringResource(R.string.button_to_remove_widget), - ) + val colors = LocalAndroidColorScheme.current + if (isDraggingToRemove) { + Button( + // Button is disabled to make it non-clickable + enabled = false, + onClick = {}, + colors = + ButtonDefaults.buttonColors( + disabledContainerColor = colors.primary, + disabledContentColor = colors.onPrimary, + ), + contentPadding = Dimensions.ButtonPadding, + modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } + ) { + RemoveButtonContent(spacerModifier) + } + } else { + OutlinedButton( + // Button is disabled to make it non-clickable + enabled = false, + onClick = {}, + colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary), + border = BorderStroke(width = 1.0.dp, color = colors.primary), + contentPadding = Dimensions.ButtonPadding, + modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } + ) { + RemoveButtonContent(spacerModifier) + } } Button( onClick = onEditDone, colors = filledButtonColors(), - contentPadding = buttonContentPadding + contentPadding = Dimensions.ButtonPadding ) { Text( text = stringResource(R.string.hub_mode_editing_exit_button_text), @@ -304,20 +328,52 @@ private fun Toolbar( } @Composable -private fun filledButtonColors(): ButtonColors { - val colors = LocalAndroidColorScheme.current - return ButtonDefaults.buttonColors( - containerColor = colors.primary, - contentColor = colors.onPrimary, +private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) { + Popup( + alignment = Alignment.TopCenter, + offset = IntOffset(0, 40), + onDismissRequest = onHidePopupAfterDismissCta + ) { + val colors = LocalAndroidColorScheme.current + Row( + modifier = + Modifier.height(56.dp) + .background(colors.secondary, RoundedCornerShape(50.dp)) + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.TouchApp, + contentDescription = stringResource(R.string.popup_on_dismiss_cta_tile_text), + tint = colors.onSecondary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(R.string.popup_on_dismiss_cta_tile_text), + style = MaterialTheme.typography.titleSmall, + color = colors.onSecondary, + ) + } + } +} + +@Composable +private fun RemoveButtonContent(spacerModifier: Modifier) { + Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_remove_widget)) + Spacer(spacerModifier) + Text( + text = stringResource(R.string.button_to_remove_widget), ) } @Composable -private fun filledSecondaryButtonColors(): ButtonColors { +private fun filledButtonColors(): ButtonColors { val colors = LocalAndroidColorScheme.current return ButtonDefaults.buttonColors( - containerColor = colors.secondary, - contentColor = colors.onSecondary, + containerColor = colors.primary, + contentColor = colors.onPrimary, ) } @@ -327,11 +383,15 @@ private fun CommunalContent( viewModel: BaseCommunalViewModel, size: SizeF, modifier: Modifier = Modifier, - elevation: Dp = 0.dp, + onOpenWidgetPicker: (() -> Unit)? = null, ) { when (model) { - is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, elevation, modifier) + is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier) is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size) + is CommunalContentModel.CtaTileInViewMode -> + CtaTileInViewModeContent(viewModel, size, modifier) + is CommunalContentModel.CtaTileInEditMode -> + CtaTileInEditModeContent(size, modifier, onOpenWidgetPicker) is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier) is CommunalContentModel.Tutorial -> TutorialContent(modifier) is CommunalContentModel.Umo -> Umo(viewModel, modifier) @@ -349,17 +409,125 @@ fun WidgetPlaceholderContent(size: SizeF) { ) {} } +/** Presents a CTA tile at the end of the grid, to customize the hub. */ +@Composable +private fun CtaTileInViewModeContent( + viewModel: BaseCommunalViewModel, + size: SizeF, + modifier: Modifier = Modifier, +) { + val colors = LocalAndroidColorScheme.current + Card( + modifier = modifier.height(size.height.dp), + colors = + CardDefaults.cardColors( + containerColor = colors.primary, + contentColor = colors.onPrimary, + ), + shape = RoundedCornerShape(80.dp, 40.dp, 80.dp, 40.dp) + ) { + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = 82.dp), + verticalArrangement = + Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Outlined.Widgets, + contentDescription = stringResource(R.string.cta_label_to_open_widget_picker), + modifier = Modifier.size(Dimensions.IconSize), + ) + Text( + text = stringResource(R.string.cta_label_to_edit_widget), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + OutlinedButton( + colors = + ButtonDefaults.buttonColors( + contentColor = colors.onPrimary, + ), + border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer), + contentPadding = Dimensions.ButtonPadding, + onClick = viewModel::onDismissCtaTile, + ) { + Text( + text = stringResource(R.string.cta_tile_button_to_dismiss), + ) + } + Spacer(modifier = Modifier.size(Dimensions.Spacing)) + Button( + colors = + ButtonDefaults.buttonColors( + containerColor = colors.primaryContainer, + contentColor = colors.onPrimaryContainer, + ), + contentPadding = Dimensions.ButtonPadding, + onClick = viewModel::onOpenWidgetEditor + ) { + Text( + text = stringResource(R.string.cta_tile_button_to_open_widget_editor), + ) + } + } + } + } +} + +/** Presents a CTA tile at the end of the hub in edit mode, to add more widgets. */ +@Composable +private fun CtaTileInEditModeContent( + size: SizeF, + modifier: Modifier = Modifier, + onOpenWidgetPicker: (() -> Unit)? = null, +) { + if (onOpenWidgetPicker == null) { + throw IllegalArgumentException("onOpenWidgetPicker should not be null.") + } + val colors = LocalAndroidColorScheme.current + Card( + modifier = modifier.height(size.height.dp), + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + border = BorderStroke(1.dp, colors.primary), + shape = RoundedCornerShape(200.dp), + onClick = onOpenWidgetPicker, + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = + Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Outlined.Widgets, + contentDescription = stringResource(R.string.cta_label_to_open_widget_picker), + tint = colors.primary, + modifier = Modifier.size(Dimensions.IconSize), + ) + Text( + text = stringResource(R.string.cta_label_to_open_widget_picker), + style = MaterialTheme.typography.titleLarge, + color = colors.primary, + textAlign = TextAlign.Center, + ) + } + } +} + @Composable private fun WidgetContent( viewModel: BaseCommunalViewModel, model: CommunalContentModel.Widget, size: SizeF, - elevation: Dp, modifier: Modifier = Modifier, ) { - Card( + Box( modifier = modifier.height(size.height.dp), - elevation = CardDefaults.cardElevation(draggedElevation = elevation), + contentAlignment = Alignment.Center, ) { AndroidView( modifier = modifier, @@ -502,4 +670,10 @@ object Dimensions { val ToolbarButtonPaddingHorizontal = 24.dp val ToolbarButtonPaddingVertical = 16.dp val ToolbarButtonSpaceBetween = 8.dp + val ButtonPadding = + PaddingValues( + vertical = ToolbarButtonPaddingVertical, + horizontal = ToolbarButtonPaddingHorizontal, + ) + val IconSize = 48.dp } 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 1b40de4ef5df..113822167ca7 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 @@ -40,6 +40,7 @@ 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.plus +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch @@ -207,7 +208,8 @@ internal constructor( fun Modifier.dragContainer( dragDropState: GridDragDropState, - beforeContentPadding: ContentPaddingInPx + beforeContentPadding: ContentPaddingInPx, + viewModel: BaseCommunalViewModel, ): Modifier { return pointerInput(dragDropState, beforeContentPadding) { detectDragGesturesAfterLongPress( @@ -220,9 +222,16 @@ fun Modifier.dragContainer( offset, Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding) ) + viewModel.onReorderWidgetStart() }, - onDragEnd = { dragDropState.onDragInterrupted() }, - onDragCancel = { dragDropState.onDragInterrupted() } + onDragEnd = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetEnd() + }, + onDragCancel = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetCancel() + } ) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 54c5de710f77..35a5054cbd2a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -487,7 +487,7 @@ class ElementTest { // page should be composed. HorizontalPager( pagerState, - beyondBoundsPageCount = 0, + outOfBoundsPageCount = 0, ) { page -> when (page) { 0 -> Box(Modifier.element(TestElements.Foo).fillMaxSize()) diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp index 927fd8ea6279..1d1849680040 100644 --- a/packages/SystemUI/customization/Android.bp +++ b/packages/SystemUI/customization/Android.bp @@ -30,8 +30,8 @@ android_library { "src/**/*.aidl", ], static_libs: [ + "PlatformAnimationLib", "PluginCoreLib", - "SystemUIAnimationLib", "SystemUIPluginLib", "SystemUIUnfoldLib", "androidx.dynamicanimation_dynamicanimation", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 36aa4416f292..cec2d7459817 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -496,7 +496,8 @@ public class UdfpsControllerTest extends SysuiTestCase { final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]}; final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90}; final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0], - sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]); + sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0], + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); for (int i1 = 0; i1 <= 1; ++i1) { for (int i2 = 0; i2 <= 1; ++i2) { @@ -505,7 +506,8 @@ public class UdfpsControllerTest extends SysuiTestCase { for (int i5 = 0; i5 <= 1; ++i5) { final UdfpsOverlayParams newParams = new UdfpsOverlayParams( sensorBounds[i1], sensorBounds[i1], displayWidth[i2], - displayHeight[i3], scaleFactor[i4], rotation[i5]); + displayHeight[i3], scaleFactor[i4], rotation[i5], + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); if (newParams.equals(oldParams)) { continue; @@ -549,7 +551,7 @@ public class UdfpsControllerTest extends SysuiTestCase { // Initialize the overlay. mUdfpsController.updateOverlayParams(mOpticalProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, - scaleFactor, rotation)); + scaleFactor, rotation, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)); // Show the overlay. mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, @@ -560,7 +562,7 @@ public class UdfpsControllerTest extends SysuiTestCase { // Update overlay with the same parameters. mUdfpsController.updateOverlayParams(mOpticalProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, - scaleFactor, rotation)); + scaleFactor, rotation, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)); mFgExecutor.runAllReady(); // Ensure the overlay was not recreated. @@ -642,7 +644,8 @@ public class UdfpsControllerTest extends SysuiTestCase { // Test ROTATION_0 mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, - scaleFactor, Surface.ROTATION_0)); + scaleFactor, Surface.ROTATION_0, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)); MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); @@ -657,7 +660,8 @@ public class UdfpsControllerTest extends SysuiTestCase { reset(mFingerprintManager); mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, - scaleFactor, Surface.ROTATION_90)); + scaleFactor, Surface.ROTATION_90, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)); event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); @@ -671,7 +675,8 @@ public class UdfpsControllerTest extends SysuiTestCase { reset(mFingerprintManager); mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, - scaleFactor, Surface.ROTATION_270)); + scaleFactor, Surface.ROTATION_270, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)); event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricExecutor.runAllReady(); @@ -685,7 +690,8 @@ public class UdfpsControllerTest extends SysuiTestCase { reset(mFingerprintManager); mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, - scaleFactor, Surface.ROTATION_180)); + scaleFactor, Surface.ROTATION_180, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL)); // ROTATION_180 is not supported. It should be treated like ROTATION_0. event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor); mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt index 455f9865edf3..92b75cb0f47d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 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. @@ -25,13 +25,13 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi 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.mockito.Mock import org.mockito.Mockito.verify @@ -59,74 +59,62 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() { } @Test - fun mediaPlaying_defaultsToFalse() = + fun hasAnyMediaOrRecommendation_defaultsToFalse() = testScope.runTest { mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - val isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying) + val mediaModel = collectLastValue(mediaRepository.mediaModel) runCurrent() - assertThat(isMediaPlaying()).isFalse() + assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() } @Test - fun mediaPlaying_emitsInitialValue() = + fun mediaModel_updatesWhenMediaDataLoaded() = testScope.runTest { - // Start with media available. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - val isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying) - runCurrent() - assertThat(isMediaPlaying()).isTrue() - } - - @Test - fun mediaPlaying_updatesWhenMediaDataLoaded() = - testScope.runTest { - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) + // Listener is added + verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) // Initial value is false. - var isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying) + val mediaModel = collectLastValue(mediaRepository.mediaModel) runCurrent() - assertThat(isMediaPlaying()).isFalse() - - // Listener is added - verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) + assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() // Change to media available and notify the listener. whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) + whenever(mediaData.createdTimestampMillis).thenReturn(1234L) mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) - - // mediaPlaying now returns true. - isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying) runCurrent() - assertThat(isMediaPlaying()).isTrue() + + // Media active now returns true. + assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() + assertThat(mediaModel()?.createdTimestampMillis).isEqualTo(1234L) } @Test - fun mediaPlaying_updatesWhenMediaDataRemoved() = + fun mediaModel_updatesWhenMediaDataRemoved() = testScope.runTest { - // Start with media available. - whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) - mediaRepository = CommunalMediaRepositoryImpl(mediaDataManager) - // Initial value is true. - var isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying) + // Listener is added + verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) + + // Change to media available and notify the listener. + whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(true) + mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) runCurrent() - assertThat(isMediaPlaying()).isTrue() - // Listener is added. - verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture()) + // Media active now returns true. + val mediaModel = collectLastValue(mediaRepository.mediaModel) + assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isTrue() // Change to media unavailable and notify the listener. whenever(mediaDataManager.hasAnyMediaOrRecommendation()).thenReturn(false) - mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData) - - // mediaPlaying now returns false. - isMediaPlaying = collectLastValue(mediaRepository.mediaPlaying) + mediaDataListenerCaptor.value.onMediaDataRemoved("key") runCurrent() - assertThat(isMediaPlaying()).isFalse() + + // Media active now returns false. + assertThat(mediaModel()?.hasAnyMediaOrRecommendation).isFalse() } } 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 449ee6f414dd..4079f1241f31 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 @@ -19,14 +19,14 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo -import android.content.BroadcastReceiver 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 -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 @@ -38,15 +38,12 @@ import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -55,8 +52,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -70,8 +67,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: AppWidgetHost - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var userManager: UserManager @Mock private lateinit var userHandle: UserHandle @@ -125,10 +120,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { communalEnabled(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - verify(communalWidgetDao, Mockito.never()).getWidgets() + verify(communalWidgetDao, never()).getWidgets() } @Test @@ -136,10 +131,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - verify(communalWidgetDao, Mockito.never()).getWidgets() + verify(communalWidgetDao, never()).getWidgets() } @Test @@ -147,8 +142,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - val communalWidgets = collectLastValue(repository.communalWidgets) - communalWidgets() + 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) @@ -158,11 +152,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { userUnlocked(true) installedProviders(listOf(stopwatchProviderInfo)) - broadcastReceiverUpdate() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) runCurrent() verify(communalWidgetDao).getWidgets() - assertThat(communalWidgets()) + assertThat(communalWidgets) .containsExactly( CommunalWidgetContentModel( appWidgetId = communalWidgetItemEntry.widgetId, @@ -182,9 +179,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) - repository.addWidget(provider, priority) + repository.addWidget(provider, priority) { true } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -192,75 +190,117 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun deleteWidget_removeWidgetId_andDeleteFromDb() = + fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { userUnlocked(true) val repository = initCommunalWidgetRepository() runCurrent() + val provider = ComponentName("pkg_name", "cls_name") val id = 1 - repository.deleteWidget(id) + val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) + whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) + repository.addWidget(provider, priority) { false } runCurrent() - verify(communalWidgetDao).deleteWidgetById(id) + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao, never()).addWidget(id, provider, priority) verify(appWidgetHost).deleteAppWidgetId(id) } @Test - fun reorderWidgets_queryDb() = + fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { userUnlocked(true) val repository = initCommunalWidgetRepository() runCurrent() - val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) - repository.updateWidgetOrder(widgetIdToPriorityMap) + 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") } runCurrent() - verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao, never()).addWidget(id, provider, priority) + verify(appWidgetHost).deleteAppWidgetId(id) } @Test - fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = + fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - communalEnabled(false) + userUnlocked(true) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verifyBroadcastReceiverNeverRegistered() + runCurrent() + + val provider = ComponentName("pkg_name", "cls_name") + val id = 1 + val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(false) + whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) + .thenReturn(id) + var configured = false + repository.addWidget(provider, priority) { + configured = true + true + } + runCurrent() + + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao).addWidget(id, provider, priority) + assertThat(configured).isFalse() } @Test - fun broadcastReceiver_featureEnabledAndUserUnlocked_doNotRegisterBroadcastReceiver() = + fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { userUnlocked(true) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verifyBroadcastReceiverNeverRegistered() + runCurrent() + + val id = 1 + repository.deleteWidget(id) + runCurrent() + + verify(communalWidgetDao).deleteWidgetById(id) + verify(appWidgetHost).deleteAppWidgetId(id) } @Test - fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() = + fun reorderWidgets_queryDb() = testScope.runTest { - userUnlocked(false) + userUnlocked(true) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verifyBroadcastReceiverRegistered() + runCurrent() + + val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) + repository.updateWidgetOrder(widgetIdToPriorityMap) + runCurrent() + + verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } @Test - fun broadcastReceiver_whenFlowFinishes_unregisterBroadcastReceiver() = + fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = testScope.runTest { - userUnlocked(false) + communalEnabled(false) val repository = initCommunalWidgetRepository() - - val job = launch { repository.communalWidgets.collect() } + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - val receiver = broadcastReceiverUpdate() + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0) + } - job.cancel() + @Test + fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() = + testScope.runTest { + userUnlocked(false) + val repository = initCommunalWidgetRepository() + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - - verify(broadcastDispatcher).unregisterReceiver(receiver) + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1) } @Test @@ -268,12 +308,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verify(appWidgetHost, Mockito.never()).startListening() + repository.communalWidgets.launchIn(backgroundScope) + runCurrent() + verify(appWidgetHost, never()).startListening() userUnlocked(true) - broadcastReceiverUpdate() - collectLastValue(repository.communalWidgets)() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) + runCurrent() verify(appWidgetHost).startListening() } @@ -283,18 +327,25 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() + repository.communalWidgets.launchIn(backgroundScope) + runCurrent() userUnlocked(true) - broadcastReceiverUpdate() - collectLastValue(repository.communalWidgets)() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) + runCurrent() verify(appWidgetHost).startListening() - verify(appWidgetHost, Mockito.never()).stopListening() + verify(appWidgetHost, never()).stopListening() userUnlocked(false) - broadcastReceiverUpdate() - collectLastValue(repository.communalWidgets)() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) + runCurrent() verify(appWidgetHost).stopListening() } @@ -305,7 +356,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { appWidgetHost, testScope.backgroundScope, testDispatcher, - broadcastDispatcher, + fakeBroadcastDispatcher, communalRepository, communalWidgetHost, communalWidgetDao, @@ -315,45 +366,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) } - private fun verifyBroadcastReceiverRegistered() { - verify(broadcastDispatcher) - .registerReceiver( - any(), - any(), - nullable(), - nullable(), - anyInt(), - nullable(), - ) - } - - private fun verifyBroadcastReceiverNeverRegistered() { - verify(broadcastDispatcher, Mockito.never()) - .registerReceiver( - any(), - any(), - nullable(), - nullable(), - anyInt(), - nullable(), - ) - } - - private fun broadcastReceiverUpdate(): BroadcastReceiver { - val broadcastReceiverCaptor = kotlinArgumentCaptor<BroadcastReceiver>() - verify(broadcastDispatcher) - .registerReceiver( - broadcastReceiverCaptor.capture(), - any(), - nullable(), - nullable(), - anyInt(), - nullable(), - ) - broadcastReceiverCaptor.value.onReceive(null, null) - return broadcastReceiverCaptor.value - } - private fun communalEnabled(enabled: Boolean) { communalRepository.setIsCommunalEnabled(enabled) } 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 62084aa0d981..744b65f20592 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 @@ -148,25 +148,29 @@ class CommunalInteractorTest : SysuiTestCase() { whenever(target1.smartspaceTargetId).thenReturn("target1") whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_WEATHER) whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java)) + whenever(target1.creationTimeMillis).thenReturn(0L) // Does not have RemoteViews val target2 = mock(SmartspaceTarget::class.java) - whenever(target1.smartspaceTargetId).thenReturn("target2") - whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target1.remoteViews).thenReturn(null) + whenever(target2.smartspaceTargetId).thenReturn("target2") + whenever(target2.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target2.remoteViews).thenReturn(null) + whenever(target2.creationTimeMillis).thenReturn(0L) // Timer and has RemoteViews val target3 = mock(SmartspaceTarget::class.java) - whenever(target1.smartspaceTargetId).thenReturn("target3") - whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java)) + whenever(target3.smartspaceTargetId).thenReturn("target3") + whenever(target3.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target3.remoteViews).thenReturn(mock(RemoteViews::class.java)) + whenever(target3.creationTimeMillis).thenReturn(0L) val targets = listOf(target1, target2, target3) smartspaceRepository.setCommunalSmartspaceTargets(targets) - val smartspaceContent by collectLastValue(underTest.smartspaceContent) + val smartspaceContent by collectLastValue(underTest.ongoingContent) assertThat(smartspaceContent?.size).isEqualTo(1) - assertThat(smartspaceContent?.get(0)?.key).isEqualTo("smartspace_target3") + assertThat(smartspaceContent?.get(0)?.key) + .isEqualTo(CommunalContentModel.KEY.smartspace("target3")) } @Test @@ -256,16 +260,12 @@ class CommunalInteractorTest : SysuiTestCase() { val targets = mutableListOf<SmartspaceTarget>() for (index in 0 until totalTargets) { - val target = mock(SmartspaceTarget::class.java) - whenever(target.smartspaceTargetId).thenReturn("target$index") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java)) - targets.add(target) + targets.add(smartspaceTimer(index.toString())) } smartspaceRepository.setCommunalSmartspaceTargets(targets) - val smartspaceContent by collectLastValue(underTest.smartspaceContent) + val smartspaceContent by collectLastValue(underTest.ongoingContent) assertThat(smartspaceContent?.size).isEqualTo(totalTargets) for (index in 0 until totalTargets) { assertThat(smartspaceContent?.get(index)?.size).isEqualTo(expectedSizes[index]) @@ -279,13 +279,77 @@ class CommunalInteractorTest : SysuiTestCase() { tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) // Media is playing. - mediaRepository.mediaPlaying.value = true + mediaRepository.mediaActive() - val umoContent by collectLastValue(underTest.umoContent) + val umoContent by collectLastValue(underTest.ongoingContent) assertThat(umoContent?.size).isEqualTo(1) assertThat(umoContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java) - assertThat(umoContent?.get(0)?.key).isEqualTo(CommunalContentModel.UMO_KEY) + assertThat(umoContent?.get(0)?.key).isEqualTo(CommunalContentModel.KEY.umo()) + } + + @Test + fun ongoing_shouldOrderAndSizeByTimestamp() = + testScope.runTest { + // Keyguard showing, and tutorial completed. + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardOccluded(false) + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + // Timer1 started + val timer1 = smartspaceTimer("timer1", timestamp = 1L) + smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1)) + + // Umo started + mediaRepository.mediaActive(timestamp = 2L) + + // Timer2 started + val timer2 = smartspaceTimer("timer2", timestamp = 3L) + smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2)) + + // Timer3 started + val timer3 = smartspaceTimer("timer3", timestamp = 4L) + smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2, timer3)) + + val ongoingContent by collectLastValue(underTest.ongoingContent) + assertThat(ongoingContent?.size).isEqualTo(4) + assertThat(ongoingContent?.get(0)?.key) + .isEqualTo(CommunalContentModel.KEY.smartspace("timer3")) + assertThat(ongoingContent?.get(0)?.size).isEqualTo(CommunalContentSize.FULL) + assertThat(ongoingContent?.get(1)?.key) + .isEqualTo(CommunalContentModel.KEY.smartspace("timer2")) + assertThat(ongoingContent?.get(1)?.size).isEqualTo(CommunalContentSize.THIRD) + assertThat(ongoingContent?.get(2)?.key).isEqualTo(CommunalContentModel.KEY.umo()) + assertThat(ongoingContent?.get(2)?.size).isEqualTo(CommunalContentSize.THIRD) + assertThat(ongoingContent?.get(3)?.key) + .isEqualTo(CommunalContentModel.KEY.smartspace("timer1")) + assertThat(ongoingContent?.get(3)?.size).isEqualTo(CommunalContentSize.THIRD) + } + + @Test + fun cta_visibilityTrue_shows() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + communalRepository.setCtaTileInViewModeVisibility(true) + + val ctaTileContent by collectLastValue(underTest.ctaTileContent) + + assertThat(ctaTileContent?.size).isEqualTo(1) + assertThat(ctaTileContent?.get(0)) + .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java) + assertThat(ctaTileContent?.get(0)?.key) + .isEqualTo(CommunalContentModel.KEY.CTA_TILE_IN_VIEW_MODE_KEY) + } + + @Test + fun ctaTile_visibilityFalse_doesNotShow() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + communalRepository.setCtaTileInViewModeVisibility(false) + + val ctaTileContent by collectLastValue(underTest.ctaTileContent) + + assertThat(ctaTileContent).isEmpty() } @Test @@ -334,4 +398,13 @@ class CommunalInteractorTest : SysuiTestCase() { underTest.showWidgetEditor() verify(editWidgetsActivityStarter).startActivity() } + + private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget { + val timer = mock(SmartspaceTarget::class.java) + whenever(timer.smartspaceTargetId).thenReturn(id) + whenever(timer.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(timer.remoteViews).thenReturn(mock(RemoteViews::class.java)) + whenever(timer.creationTimeMillis).thenReturn(timestamp) + return timer + } } 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 f2f97054ea12..ff6fd43745df 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 @@ -16,12 +16,17 @@ 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.os.PowerManager import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository @@ -29,6 +34,7 @@ import com.android.systemui.communal.data.repository.FakeCommunalTutorialReposit import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory 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.coroutines.collectLastValue @@ -42,20 +48,26 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import javax.inject.Provider +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 import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var appWidgetHost: AppWidgetHost + @Mock private lateinit var uiEventLogger: UiEventLogger private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -73,7 +85,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - val withDeps = CommunalInteractorFactory.create() + val withDeps = CommunalInteractorFactory.create(testScope) keyguardRepository = withDeps.keyguardRepository communalRepository = withDeps.communalRepository tutorialRepository = withDeps.tutorialRepository @@ -84,14 +96,16 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { underTest = CommunalEditModeViewModel( withDeps.communalInteractor, + appWidgetHost, Provider { shadeViewController }, powerManager, mediaHost, + uiEventLogger, ) } @Test - fun communalContent_onlyWidgetsAreShownInEditMode() = + fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) @@ -119,16 +133,18 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { smartspaceRepository.setCommunalSmartspaceTargets(listOf(target)) // Media playing. - mediaRepository.mediaPlaying.value = true + mediaRepository.mediaActive() val communalContent by collectLastValue(underTest.communalContent) - // Only Widgets are shown. - assertThat(communalContent?.size).isEqualTo(2) + // Only Widgets and CTA tile are shown. + assertThat(communalContent?.size).isEqualTo(3) assertThat(communalContent?.get(0)) .isInstanceOf(CommunalContentModel.Widget::class.java) assertThat(communalContent?.get(1)) .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(2)) + .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java) } @Test @@ -143,4 +159,71 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { ) .isEqualTo(false) } + + @Test + fun addingWidgetTriggersConfiguration() = + testScope.runTest { + val provider = ComponentName("pkg.test", "testWidget") + val widgetToConfigure by collectLastValue(underTest.widgetsToConfigure) + assertThat(widgetToConfigure).isNull() + underTest.onAddWidget(componentName = provider, priority = 0) + assertThat(widgetToConfigure).isEqualTo(1) + } + + @Test + fun settingResultOkAddsWidget() = + testScope.runTest { + val provider = ComponentName("pkg.test", "testWidget") + val widgetAdded by collectLastValue(widgetRepository.widgetAdded) + assertThat(widgetAdded).isNull() + underTest.onAddWidget(componentName = provider, priority = 0) + assertThat(widgetAdded).isNull() + underTest.setConfigurationResult(RESULT_OK) + assertThat(widgetAdded).isEqualTo(1) + } + + @Test + fun settingResultCancelledDoesNotAddWidget() = + testScope.runTest { + val provider = ComponentName("pkg.test", "testWidget") + val widgetAdded by collectLastValue(widgetRepository.widgetAdded) + assertThat(widgetAdded).isNull() + underTest.onAddWidget(componentName = provider, priority = 0) + assertThat(widgetAdded).isNull() + underTest.setConfigurationResult(RESULT_CANCELED) + assertThat(widgetAdded).isNull() + } + + @Test(expected = IllegalStateException::class) + fun settingResultBeforeWidgetAddedThrowsException() { + underTest.setConfigurationResult(RESULT_OK) + } + + @Test(expected = IllegalStateException::class) + fun addingWidgetWhileConfigurationActiveFails() = + testScope.runTest { + val providerOne = ComponentName("pkg.test", "testWidget") + underTest.onAddWidget(componentName = providerOne, priority = 0) + runCurrent() + val providerTwo = ComponentName("pkg.test", "testWidget2") + underTest.onAddWidget(componentName = providerTwo, priority = 0) + } + + @Test + fun reorderWidget_uiEventLogging_start() { + underTest.onReorderWidgetStart() + verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) + } + + @Test + fun reorderWidget_uiEventLogging_end() { + underTest.onReorderWidgetEnd() + verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) + } + + @Test + fun reorderWidget_uiEventLogging_cancel() { + underTest.onReorderWidgetCancel() + verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) + } } 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 182cc5d750bb..8e3f66464de6 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 @@ -31,6 +31,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository @@ -41,7 +42,9 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -50,6 +53,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalViewModelTest : SysuiTestCase() { @@ -84,6 +88,7 @@ class CommunalViewModelTest : SysuiTestCase() { underTest = CommunalViewModel( + testScope, withDeps.communalInteractor, WidgetInteractionHandler(mock()), withDeps.tutorialInteractor, @@ -112,7 +117,7 @@ class CommunalViewModelTest : SysuiTestCase() { } @Test - fun ordering_smartspaceBeforeUmoBeforeWidgets() = + fun ordering_smartspaceBeforeUmoBeforeWidgetsBeforeCtaTile() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) @@ -140,12 +145,15 @@ class CommunalViewModelTest : SysuiTestCase() { smartspaceRepository.setCommunalSmartspaceTargets(listOf(target)) // Media playing. - mediaRepository.mediaPlaying.value = true + mediaRepository.mediaActive() + + // CTA Tile not dismissed. + communalRepository.setCtaTileInViewModeVisibility(true) val communalContent by collectLastValue(underTest.communalContent) - // Order is smart space, then UMO, then widget content. - assertThat(communalContent?.size).isEqualTo(4) + // Order is smart space, then UMO, widget content and cta tile. + assertThat(communalContent?.size).isEqualTo(5) assertThat(communalContent?.get(0)) .isInstanceOf(CommunalContentModel.Smartspace::class.java) assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java) @@ -153,5 +161,47 @@ class CommunalViewModelTest : SysuiTestCase() { .isInstanceOf(CommunalContentModel.Widget::class.java) assertThat(communalContent?.get(3)) .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(4)) + .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java) + } + + @Test + 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) + + assertThat(communalContent?.size).isEqualTo(1) + assertThat(communalContent?.get(0)) + .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java) + + underTest.onDismissCtaTile() + + // hide CTA tile and show the popup + assertThat(communalContent).isEmpty() + assertThat(isPopupOnDismissCtaShowing).isEqualTo(true) + + // hide popup after time elapsed + advanceTimeBy(POPUP_AUTO_HIDE_TIMEOUT_MS) + assertThat(isPopupOnDismissCtaShowing).isEqualTo(false) + } + + @Test + fun popup_onDismiss_hidesImmediately() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + communalRepository.setCtaTileInViewModeVisibility(true) + + val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) + + underTest.onDismissCtaTile() + assertThat(isPopupOnDismissCtaShowing).isEqualTo(true) + + // dismiss the popup directly + underTest.onHidePopupAfterDismissCta() + assertThat(isPopupOnDismissCtaShowing).isEqualTo(false) } } 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 6c4bb372bc3a..c4ebbdcc2f58 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 @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository import com.android.systemui.common.shared.model.Position import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.DozeMachine @@ -71,6 +72,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) private lateinit var systemClock: FakeSystemClock + private lateinit var facePropertyRepository: FakeFacePropertyRepository private lateinit var underTest: KeyguardRepositoryImpl @@ -78,6 +80,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) systemClock = FakeSystemClock() + facePropertyRepository = FakeFacePropertyRepository() underTest = KeyguardRepositoryImpl( statusBarStateController, @@ -89,6 +92,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { mainDispatcher, testScope.backgroundScope, systemClock, + facePropertyRepository, ) } @@ -482,10 +486,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { testScope.runTest { val values = mutableListOf<Point?>() val job = underTest.faceSensorLocation.onEach(values::add).launchIn(this) - - val captor = argumentCaptor<AuthController.Callback>() runCurrent() - verify(authController).addCallback(captor.capture()) // An initial, null value should be initially emitted so that flows combined with this // one @@ -500,8 +501,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { Point(250, 250), ) .onEach { - whenever(authController.faceSensorLocation).thenReturn(it) - captor.value.onFaceSensorLocationChanged() + facePropertyRepository.setSensorLocation(it) runCurrent() } .also { dispatchedSensorLocations -> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 7c3dc972cfd0..5b88ebe69bfe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -32,7 +32,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.statusbar.notification.data.repository.fakeNotificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.testKosmos @@ -56,8 +56,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val screenOffAnimationController = kosmos.screenOffAnimationController private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository - private val fakeNotificationsKeyguardViewStateRepository = - kosmos.fakeNotificationsKeyguardViewStateRepository + private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor private val dozeParameters = kosmos.dozeParameters private val underTest = kosmos.keyguardRootViewModel @@ -118,7 +117,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(true) + notificationsKeyguardInteractor.setPulseExpanding(true) deviceEntryRepository.setBypassEnabled(false) runCurrent() @@ -130,9 +129,9 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(true) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -144,10 +143,10 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(false) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -159,11 +158,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(true) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -175,11 +174,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -191,11 +190,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.isAnimating).isEqualTo(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt index 00405d0a07e7..c2ce39249f9e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt @@ -29,27 +29,37 @@ import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R +import com.android.systemui.util.time.FakeSystemClock import java.time.Instant import java.time.LocalDateTime import java.util.TimeZone +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class AlarmTileMapperTest : SysuiTestCase() { + private val oneMinute = 60000L private val kosmos = Kosmos() private val alarmTileConfig = kosmos.qsAlarmTileConfig + private val fakeClock = FakeSystemClock() // Using lazy (versus =) to make sure we override the right context -- see b/311612168 private val mapper by lazy { AlarmTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.ic_alarm, TestStubDrawable()) } .resources, - context.theme + context.theme, + fakeClock ) } + @Before + fun setup() { + fakeClock.setCurrentTimeMillis(0) // same time both in test & map() + } + @Test fun notAlarmSet() { val inputModel = AlarmTileModel.NoAlarmSet @@ -66,7 +76,7 @@ class AlarmTileMapperTest : SysuiTestCase() { @Test fun nextAlarmSet24HourFormat() { - val triggerTime = 1L + val triggerTime = fakeClock.currentTimeMillis() + oneMinute val inputModel = AlarmTileModel.NextAlarmSet(true, AlarmManager.AlarmClockInfo(triggerTime, null)) @@ -85,7 +95,7 @@ class AlarmTileMapperTest : SysuiTestCase() { @Test fun nextAlarmSet12HourFormat() { - val triggerTime = 1L + val triggerTime = fakeClock.currentTimeMillis() + oneMinute val inputModel = AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null)) @@ -102,6 +112,66 @@ class AlarmTileMapperTest : SysuiTestCase() { QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } + @Test + fun nextAlarmSetMoreThanAWeekLater_mapsSecondaryLabelToDisplayDateOnly() { + val oneWeekAndOneMinute = 7 * 24 * 60 * 60 * 1000L + oneMinute + val triggerTime = fakeClock.currentTimeMillis() + oneWeekAndOneMinute + val inputModel = + AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null)) + + val outputState = mapper.map(alarmTileConfig, inputModel) + + val localDateTime = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(triggerTime), + TimeZone.getDefault().toZoneId() + ) + val expectedSecondaryLabel = AlarmTileMapper.formatterDateOnly.format(localDateTime) + val expectedState = + createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun nextAlarmSetOneMinuteLessThanAWeekLater_mapsSecondaryLabelToDisplayTime() { + val oneWeekMinusOneMinute = 7 * 24 * 60 * 60 * 1000L - oneMinute + val triggerTime = fakeClock.currentTimeMillis() + oneWeekMinusOneMinute + val inputModel = + AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null)) + + val outputState = mapper.map(alarmTileConfig, inputModel) + + val localDateTime = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(triggerTime), + TimeZone.getDefault().toZoneId() + ) + val expectedSecondaryLabel = AlarmTileMapper.formatter12Hour.format(localDateTime) + val expectedState = + createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun nextAlarmSetExactlyAWeekLater_mapsSecondaryLabelToDisplayDateOnly() { + val oneWeek = 7 * 24 * 60 * 60 * 1000L + val triggerTime = fakeClock.currentTimeMillis() + oneWeek + val inputModel = + AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null)) + + val outputState = mapper.map(alarmTileConfig, inputModel) + + val localDateTime = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(triggerTime), + TimeZone.getDefault().toZoneId() + ) + val expectedSecondaryLabel = AlarmTileMapper.formatterDateOnly.format(localDateTime) + val expectedState = + createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + private fun createAlarmTileState( activationState: QSTileState.ActivationState, secondaryLabel: String diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java index de767e39499a..7274c0c65e69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java @@ -21,29 +21,30 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.TestableLooper.RunWithLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.Dependency; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.model.SysUiState; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -61,17 +62,17 @@ import java.util.concurrent.atomic.AtomicBoolean; public class SystemUIDialogTest extends SysuiTestCase { @Mock - private FeatureFlags mFeatureFlags; - @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private SystemUIDialog.Delegate mDelegate; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setup() { MockitoAnnotations.initMocks(this); - mDependency.injectTestDependency(FeatureFlags.class, mFeatureFlags); mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher); } @@ -110,16 +111,13 @@ public class SystemUIDialogTest extends SysuiTestCase { } @Test + @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS) public void usePredictiveBackAnimFlag() { - when(mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM)) - .thenReturn(true); final SystemUIDialog dialog = new SystemUIDialog(mContext); dialog.show(); assertTrue(dialog.isShowing()); - verify(mFeatureFlags, atLeast(1)) - .isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM); dialog.dismiss(); assertFalse(dialog.isShowing()); @@ -174,7 +172,6 @@ public class SystemUIDialogTest extends SysuiTestCase { private SystemUIDialog createDialogWithDelegate() { SystemUIDialog.Factory factory = new SystemUIDialog.Factory( getContext(), - mFeatureFlags, Dependency.get(SystemUIDialogManager.class), Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 75d1869adc7c..a9ee4055d1a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -68,6 +68,8 @@ class FakeMobileIconsInteractor( override val isSingleCarrier = MutableStateFlow(true) + override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList()) + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping @@ -80,8 +82,12 @@ class FakeMobileIconsInteractor( override val isForceHidden = MutableStateFlow(false) /** Always returns a new fake interactor */ - override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { - return FakeMobileIconInteractor(tableLogBuffer).also { interactorCache[subId] = it } + override fun getMobileConnectionInteractorForSubId(subId: Int): FakeMobileIconInteractor { + return FakeMobileIconInteractor(tableLogBuffer).also { + interactorCache[subId] = it + // Also update the icons + icons.value = interactorCache.values.toList() + } } /** diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 0537f17b3594..9063a02ee885 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -46,8 +46,8 @@ java_library { static_libs: [ "androidx.annotation_annotation", "androidx-constraintlayout_constraintlayout", + "PlatformAnimationLib", "PluginCoreLib", - "SystemUIAnimationLib", "SystemUICommon", "SystemUILogLib", "androidx.annotation_annotation", diff --git a/packages/SystemUI/res/color/notification_state_color_dark.xml b/packages/SystemUI/res/color/notification_state_color_dark.xml new file mode 100644 index 000000000000..d26cbd5578df --- /dev/null +++ b/packages/SystemUI/res/color/notification_state_color_dark.xml @@ -0,0 +1,25 @@ +<?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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <!-- Pressed state's alpha is set to 0.00 temporarily until this bug is resolved permanently + b/313920497 Design intended alpha is 0.15--> + <item android:state_pressed="true" android:color="#ffffff" android:alpha="0.00" /> + <item android:state_hovered="true" android:color="#ffffff" android:alpha="0.11" /> + <item android:color="@color/transparent" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/notification_overlay_color.xml b/packages/SystemUI/res/color/notification_state_color_default.xml index a14a7ad9d2da..a14a7ad9d2da 100644 --- a/packages/SystemUI/res/color/notification_overlay_color.xml +++ b/packages/SystemUI/res/color/notification_state_color_default.xml diff --git a/packages/SystemUI/res/color/notification_state_color_light.xml b/packages/SystemUI/res/color/notification_state_color_light.xml new file mode 100644 index 000000000000..3e8bcf3e69e2 --- /dev/null +++ b/packages/SystemUI/res/color/notification_state_color_light.xml @@ -0,0 +1,25 @@ +<?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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <!-- Pressed state's alpha is set to 0.00 temporarily until this bug is resolved permanently + b/313920497 Design intended alpha is 0.15--> + <item android:state_pressed="true" android:color="#000000" android:alpha="0.00" /> + <item android:state_hovered="true" android:color="#000000" android:alpha="0.11" /> + <item android:color="@color/transparent" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/notification_material_bg.xml b/packages/SystemUI/res/drawable/notification_material_bg.xml index 355e75d0716b..3f903aece0b5 100644 --- a/packages/SystemUI/res/drawable/notification_material_bg.xml +++ b/packages/SystemUI/res/drawable/notification_material_bg.xml @@ -25,7 +25,7 @@ </item> <item> <shape> - <solid android:color="@color/notification_overlay_color" /> + <solid android:color="@color/notification_state_color_default" /> </shape> </item> </layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index 7ab44e70e6fe..73874a08b0bd 100644 --- a/packages/SystemUI/res/layout/qs_footer_impl.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -44,6 +44,8 @@ android:ellipsize="marquee" android:focusable="true" android:gravity="center_vertical" + android:textDirection="locale" + android:textAlignment="viewStart" android:singleLine="true" android:textAppearance="@style/TextAppearance.QS.Status.Build" android:visibility="gone" /> diff --git a/packages/SystemUI/res/layout/remote_input.xml b/packages/SystemUI/res/layout/remote_input.xml index f4b0a45a8d32..84681d34be8e 100644 --- a/packages/SystemUI/res/layout/remote_input.xml +++ b/packages/SystemUI/res/layout/remote_input.xml @@ -89,7 +89,6 @@ android:textColorHint="@color/remote_input_hint" android:textSize="16sp" android:background="@null" - android:maxLines="4" android:ellipsize="start" android:inputType="textShortMessage|textMultiLine|textAutoCorrect|textCapSentences" android:imeOptions="actionSend|flagNoExtractUi|flagNoFullscreen" /> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 10f7c4d3ee5b..17719d11345b 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -538,15 +538,15 @@ --> <string translatable="false" name="config_frontBuiltInDisplayCutoutProtection"></string> - <!-- ID for the camera of outer display that needs extra protection --> + <!-- ID for the camera of outer display that needs extra protection --> <string translatable="false" name="config_protectedCameraId"></string> - <!-- Physical ID for the camera of outer display that needs extra protection --> + <!-- Physical ID for the camera of outer display that needs extra protection --> <string translatable="false" name="config_protectedPhysicalCameraId"></string> <!-- Similar to config_frontBuiltInDisplayCutoutProtection but for inner display. --> <string translatable="false" name="config_innerBuiltInDisplayCutoutProtection"></string> - <!-- ID for the camera of inner display that needs extra protection --> + <!-- ID for the camera of inner display that needs extra protection. --> <string translatable="false" name="config_protectedInnerCameraId"></string> <!-- Physical ID for the camera of inner display that needs extra protection --> <string translatable="false" name="config_protectedInnerPhysicalCameraId"></string> @@ -650,13 +650,20 @@ <!-- Whether to use window background blur for the volume dialog. --> <bool name="config_volumeDialogUseBackgroundBlur">false</bool> - <!-- The properties of the face auth camera in pixels --> + <!-- The properties of the face auth front camera for outer display in pixels --> <integer-array name="config_face_auth_props"> <!-- sensorLocationX --> <!-- sensorLocationY --> <!--sensorRadius --> </integer-array> + <!-- The properties of the face auth front camera for inner display in pixels --> + <integer-array name="config_inner_face_auth_props"> + <!-- sensorLocationX --> + <!-- sensorLocationY --> + <!--sensorRadius --> + </integer-array> + <!-- Overrides the behavior of the face unlock keyguard bypass setting: 0 - Don't override the setting (default) 1 - Override the setting to always bypass keyguard @@ -995,4 +1002,7 @@ <item>com.android.switchaccess.SwitchAccessService</item> <item>com.google.android.apps.accessibility.voiceaccess.JustSpeakService</item> </string-array> + + <!-- Whether to use a machine learning model for back gesture falsing. --> + <bool name="config_useBackGestureML">true</bool> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 854bb0f05c5e..5e109053c994 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1079,6 +1079,16 @@ <!-- Description for the button that opens the widget editor on click. [CHAR LIMIT=50] --> <string name="button_to_open_widget_editor">Open the widget editor</string> + <!-- Text for CTA button that launches the hub mode widget editor on click. [CHAR LIMIT=50] --> + <string name="cta_tile_button_to_open_widget_editor">Customize</string> + <!-- Text for CTA button that dismisses the tile on click. [CHAR LIMIT=50] --> + <string name="cta_tile_button_to_dismiss">Dismiss</string> + <!-- Label for CTA tile to edit the glanceable hub [CHAR LIMIT=100] --> + <string name="cta_label_to_edit_widget">Add, remove, and reorder your widgets in this space</string> + <!-- Label for CTA tile that opens widget picker on click in edit mode [CHAR LIMIT=50] --> + <string name="cta_label_to_open_widget_picker">Add more widgets</string> + <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] --> + <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string> <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> <string name="button_to_remove_widget">Remove</string> <!-- Text for the button that launches the hub mode widget picker. [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 3f026a4cec8a..7d7c050a666c 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -921,7 +921,7 @@ <style name="Theme.ControlsActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar"> <item name="android:windowActivityTransitions">true</item> <item name="android:windowContentTransitions">false</item> - <item name="android:windowIsTranslucent">false</item> + <item name="android:windowIsTranslucent">true</item> <item name="android:windowBackground">@android:color/black</item> <item name="android:windowAnimationStyle">@null</item> <item name="android:statusBarColor">@android:color/black</item> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 3a26ebff6c6a..05106c904d3d 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -51,8 +51,8 @@ android_library { ], static_libs: [ "BiometricsSharedLib", + "PlatformAnimationLib", "PluginCoreLib", - "SystemUIAnimationLib", "SystemUIPluginLib", "SystemUIUnfoldLib", "SystemUISharedLib-Keyguard", diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt index 65c5a49b1135..e31fb89b5432 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/UdfpsOverlayParams.kt @@ -17,6 +17,8 @@ package com.android.systemui.biometrics.shared.model import android.graphics.Rect +import android.hardware.fingerprint.FingerprintSensorProperties.SensorType +import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL import android.view.Surface import android.view.Surface.Rotation @@ -39,6 +41,8 @@ import android.view.Surface.Rotation * the native resolution. * * [rotation] current rotation of the display. + * + * [sensorType] fingerprint sensor type */ data class UdfpsOverlayParams( val sensorBounds: Rect = Rect(), @@ -46,7 +50,8 @@ data class UdfpsOverlayParams( val naturalDisplayWidth: Int = 0, val naturalDisplayHeight: Int = 0, val scaleFactor: Float = 1f, - @Rotation val rotation: Int = Surface.ROTATION_0 + @Rotation val rotation: Int = Surface.ROTATION_0, + @SensorType val sensorType: Int = TYPE_UDFPS_OPTICAL ) { /** Same as [sensorBounds], but in native resolution. */ diff --git a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/KeyButtonRipple.java index f005af3780cb..92473e84cbd9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/KeyButtonRipple.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.navigationbar.buttons; +package com.android.systemui.shared.navigationbar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -125,7 +125,7 @@ public class KeyButtonRipple extends Drawable { /** * @param onInvisibleRunnable run after we are next drawn invisibly. Only used once. */ - void setOnInvisibleRunnable(Runnable onInvisibleRunnable) { + public void setOnInvisibleRunnable(Runnable onInvisibleRunnable) { mOnInvisibleRunnable = onInvisibleRunnable; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java index a4b6451caaea..2145166e9bc5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java @@ -30,7 +30,7 @@ import android.widget.ImageView; import androidx.annotation.DimenRes; -import com.android.systemui.navigationbar.buttons.KeyButtonRipple; +import com.android.systemui.shared.navigationbar.KeyButtonRipple; public class FloatingRotationButtonView extends ImageView { diff --git a/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java b/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java index 047ff75468ed..9f8220150b88 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/statusbar/policy/CallbackController.java @@ -21,6 +21,11 @@ import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.LifecycleOwner; +/** + * Implementation of the collection used and thread guarantees are left to the discretion of the + * client. Consider using {@link com.android.systemui.util.ListenerSet} to prevent concurrent + * modification exceptions. + */ public interface CallbackController<T> { /** Add a callback */ diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index e03c62783475..d6d5c2631e14 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -68,7 +68,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.settingslib.Utils; -import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.data.repository.FacePropertyRepository; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.decor.CutoutDecorProviderFactory; import com.android.systemui.decor.DebugRoundedCornerDelegate; @@ -92,6 +92,7 @@ import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.SecureSettings; import dalvik.annotation.optimization.NeverCompile; @@ -131,8 +132,6 @@ public class ScreenDecorations implements }; private final ScreenDecorationsLogger mLogger; - private final AuthController mAuthController; - private DisplayTracker mDisplayTracker; @VisibleForTesting protected boolean mIsRegistered; @@ -183,6 +182,9 @@ public class ScreenDecorations implements private DisplayCutout mDisplayCutout; private boolean mPendingManualConfigUpdate; + private FacePropertyRepository mFacePropertyRepository; + private JavaAdapter mJavaAdapter; + @VisibleForTesting protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { if (mFaceScanningFactory.shouldShowFaceScanningAnim()) { @@ -330,7 +332,8 @@ public class ScreenDecorations implements PrivacyDotDecorProviderFactory dotFactory, FaceScanningProviderFactory faceScanningFactory, ScreenDecorationsLogger logger, - AuthController authController) { + FacePropertyRepository facePropertyRepository, + JavaAdapter javaAdapter) { mContext = context; mSecureSettings = secureSettings; mCommandRegistry = commandRegistry; @@ -342,22 +345,10 @@ public class ScreenDecorations implements mFaceScanningFactory = faceScanningFactory; mFaceScanningViewId = com.android.systemui.res.R.id.face_scanning_anim; mLogger = logger; - mAuthController = authController; + mFacePropertyRepository = facePropertyRepository; + mJavaAdapter = javaAdapter; } - - private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() { - @Override - public void onFaceSensorLocationChanged() { - mLogger.onSensorLocationChanged(); - if (mExecutor != null) { - mExecutor.execute( - () -> updateOverlayProviderViews( - new Integer[]{mFaceScanningViewId})); - } - } - }; - private final ScreenDecorCommand.Callback mScreenDecorCommandCallback = (cmd, pw) -> { // If we are exiting debug mode, we can set it (false) and bail, otherwise we will // ensure that debug mode is set @@ -407,7 +398,8 @@ public class ScreenDecorations implements mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler); mExecutor.execute(this::startOnScreenDecorationsThread); mDotViewController.setUiExecutor(mExecutor); - mAuthController.addCallback(mAuthControllerCallback); + mJavaAdapter.alwaysCollectFlow(mFacePropertyRepository.getSensorLocation(), + this::onFaceSensorLocationChanged); mCommandRegistry.registerCommand(ScreenDecorCommand.SCREEN_DECOR_CMD_NAME, () -> new ScreenDecorCommand(mScreenDecorCommandCallback)); } @@ -1320,6 +1312,16 @@ public class ScreenDecorations implements view.setLayoutParams(params); } + @VisibleForTesting + void onFaceSensorLocationChanged(Point location) { + mLogger.onSensorLocationChanged(); + if (mExecutor != null) { + mExecutor.execute( + () -> updateOverlayProviderViews( + new Integer[]{mFaceScanningViewId})); + } + } + public static class DisplayCutoutView extends DisplayCutoutBaseView { final List<Rect> mBounds = new ArrayList(); final Rect mBoundingRect = new Rect(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index a4f90ebfb83c..093a1ffb4635 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -148,10 +148,6 @@ public class AuthController implements private final Display mDisplay; private float mScaleFactor = 1f; - // sensor locations without any resolution scaling nor rotation adjustments: - @Nullable private final Point mFaceSensorLocationDefault; - // cached sensor locations: - @Nullable private Point mFaceSensorLocation; @Nullable private Point mFingerprintSensorLocation; @Nullable private Rect mUdfpsBounds; private final Set<Callback> mCallbacks = new HashSet<>(); @@ -622,7 +618,6 @@ public class AuthController implements mScaleFactor = mUdfpsUtils.getScaleFactor(mCachedDisplayInfo); updateUdfpsLocation(); updateFingerprintLocation(); - updateFaceLocation(); } /** * @return where the fingerprint sensor exists in pixels in its natural orientation. @@ -682,31 +677,6 @@ public class AuthController implements } /** - * @return where the face sensor exists in pixels in the current device orientation. Returns - * null if no face sensor exists. - */ - @Nullable public Point getFaceSensorLocation() { - return mFaceSensorLocation; - } - - private void updateFaceLocation() { - if (mFaceProps == null || mFaceSensorLocationDefault == null) { - mFaceSensorLocation = null; - } else { - mFaceSensorLocation = rotateToCurrentOrientation( - new Point( - (int) (mFaceSensorLocationDefault.x * mScaleFactor), - (int) (mFaceSensorLocationDefault.y * mScaleFactor)), - mCachedDisplayInfo - ); - } - - for (final Callback cb : mCallbacks) { - cb.onFaceSensorLocationChanged(); - } - } - - /** * @param inOutPoint point on the display in pixels. Going in, represents the point * in the device's natural orientation. Going out, represents * the point in the display's current orientation. @@ -821,17 +791,7 @@ public class AuthController implements mWakefulnessLifecycle = wakefulnessLifecycle; mPanelInteractionDetector = panelInteractionDetector; - mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null; - int[] faceAuthLocation = context.getResources().getIntArray( - com.android.systemui.res.R.array.config_face_auth_props); - if (faceAuthLocation == null || faceAuthLocation.length < 2) { - mFaceSensorLocationDefault = null; - } else { - mFaceSensorLocationDefault = new Point( - faceAuthLocation[0], - faceAuthLocation[1]); - } mDisplay = mContext.getDisplay(); updateSensorLocations(); @@ -868,7 +828,8 @@ public class AuthController implements mCachedDisplayInfo.getNaturalWidth(), mCachedDisplayInfo.getNaturalHeight(), mScaleFactor, - mCachedDisplayInfo.rotation); + mCachedDisplayInfo.rotation, + udfpsProp.sensorType); mUdfpsController.updateOverlayParams(udfpsProp, mUdfpsOverlayParams); if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds) || !Objects.equals( @@ -1358,8 +1319,6 @@ public class AuthController implements final AuthDialog dialog = mCurrentDialog; pw.println(" mCachedDisplayInfo=" + mCachedDisplayInfo); pw.println(" mScaleFactor=" + mScaleFactor); - pw.println(" faceAuthSensorLocationDefault=" + mFaceSensorLocationDefault); - pw.println(" faceAuthSensorLocation=" + getFaceSensorLocation()); pw.println(" fingerprintSensorLocationInNaturalOrientation=" + getFingerprintSensorLocationInNaturalOrientation()); pw.println(" fingerprintSensorLocation=" + getFingerprintSensorLocation()); @@ -1433,11 +1392,5 @@ public class AuthController implements * {@link #onFingerprintLocationChanged}. */ default void onUdfpsLocationChanged(UdfpsOverlayParams udfpsOverlayParams) {} - - /** - * Called when the location of the face unlock sensor (typically the front facing camera) - * changes. The location in pixels can change due to resolution changes. - */ - default void onFaceSensorLocationChanged() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 45967c600a3c..86f372a94848 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -32,6 +32,7 @@ import com.android.keyguard.logging.KeyguardLogger import com.android.settingslib.Utils import com.android.systemui.CoreStartable import com.android.systemui.Flags.lightRevealMigration +import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor @@ -80,6 +81,7 @@ class AuthRippleController @Inject constructor( private val logger: KeyguardLogger, private val biometricUnlockController: BiometricUnlockController, private val lightRevealScrim: LightRevealScrim, + private val facePropertyRepository: FacePropertyRepository, rippleView: AuthRippleView? ) : ViewController<AuthRippleView>(rippleView), @@ -263,7 +265,7 @@ class AuthRippleController @Inject constructor( fun updateSensorLocation() { fingerprintSensorLocation = authController.fingerprintSensorLocation - faceSensorLocation = authController.faceSensorLocation + faceSensorLocation = facePropertyRepository.sensorLocation.value } private fun updateRippleColor() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt index b0143f5cdc4a..aaccbc1d2f9e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt @@ -22,6 +22,7 @@ import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager.DisplayListener import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED import android.os.Handler +import android.util.Size import android.view.DisplayInfo import com.android.internal.util.ArrayUtils import com.android.systemui.biometrics.shared.model.DisplayRotation @@ -40,6 +41,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Repository for the current state of the display */ @@ -58,6 +60,9 @@ interface DisplayStateRepository { /** Provides the current display rotation */ val currentRotation: StateFlow<DisplayRotation> + + /** Provides the current display size */ + val currentDisplaySize: StateFlow<Size> } // TODO(b/296211844): This class could directly use DeviceStateRepository and DisplayRepository @@ -110,17 +115,13 @@ constructor( initialValue = false, ) - private fun getDisplayRotation(): DisplayRotation { + private fun getDisplayInfo(): DisplayInfo { val cachedDisplayInfo = DisplayInfo() context.display?.getDisplayInfo(cachedDisplayInfo) - var rotation = cachedDisplayInfo.rotation - if (isReverseDefaultRotation) { - rotation = (rotation + 1) % 4 - } - return rotation.toDisplayRotation() + return cachedDisplayInfo } - override val currentRotation: StateFlow<DisplayRotation> = + private val currentDisplayInfo: StateFlow<DisplayInfo> = conflatedCallbackFlow { val callback = object : DisplayListener { @@ -129,11 +130,11 @@ constructor( override fun onDisplayAdded(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - val rotation = getDisplayRotation() + val displayInfo = getDisplayInfo() trySendWithFailureLogging( - rotation, + displayInfo, TAG, - "Error sending display rotation to $rotation" + "Error sending displayInfo to $displayInfo" ) } } @@ -148,7 +149,37 @@ constructor( .stateIn( applicationScope, started = SharingStarted.Eagerly, - initialValue = getDisplayRotation(), + initialValue = getDisplayInfo(), + ) + + private fun rotationToDisplayRotation(rotation: Int): DisplayRotation { + var adjustedRotation = rotation + if (isReverseDefaultRotation) { + adjustedRotation = (rotation + 1) % 4 + } + return adjustedRotation.toDisplayRotation() + } + + override val currentRotation: StateFlow<DisplayRotation> = + currentDisplayInfo + .map { rotationToDisplayRotation(it.rotation) } + .stateIn( + applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = rotationToDisplayRotation(currentDisplayInfo.value.rotation) + ) + + override val currentDisplaySize: StateFlow<Size> = + currentDisplayInfo + .map { Size(it.naturalWidth, it.naturalHeight) } + .stateIn( + applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = + Size( + currentDisplayInfo.value.naturalWidth, + currentDisplayInfo.value.naturalHeight + ), ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt index 0ae2e1614fba..ae1539ebaf89 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt @@ -17,25 +17,39 @@ package com.android.systemui.biometrics.data.repository +import android.content.Context +import android.graphics.Point +import android.hardware.camera2.CameraManager import android.hardware.face.FaceManager import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.face.IFaceAuthenticatorsRegisteredCallback import android.util.Log +import android.util.RotationUtils +import android.util.Size +import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.LockoutMode import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.biometrics.shared.model.toLockoutMode +import com.android.systemui.biometrics.shared.model.toRotation import com.android.systemui.biometrics.shared.model.toSensorStrength import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose 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.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -47,20 +61,38 @@ interface FacePropertyRepository { /** Get the current lockout mode for the user. This makes a binder based service call. */ suspend fun getLockoutMode(userId: Int): LockoutMode + + /** The current face sensor location in current device rotation */ + val sensorLocation: StateFlow<Point?> } /** Describes a biometric sensor */ data class FaceSensorInfo(val id: Int, val strength: SensorStrength) +/** Data class for camera info */ +private data class CameraInfo( + /** The logical id of the camera */ + val cameraId: String, + /** The physical id of the camera */ + val cameraPhysicalId: String?, + /** The center point of the camera in natural orientation */ + val cameraLocation: Point?, +) + private const val TAG = "FaceSensorPropertyRepositoryImpl" @SysUISingleton class FacePropertyRepositoryImpl @Inject constructor( + @Application val applicationContext: Context, + @Main mainExecutor: Executor, @Application private val applicationScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val faceManager: FaceManager?, + private val cameraManager: CameraManager, + displayStateRepository: DisplayStateRepository, + configurationRepository: ConfigurationRepository, ) : FacePropertyRepository { override val sensorInfo: StateFlow<FaceSensorInfo?> = @@ -89,10 +121,179 @@ constructor( .onEach { Log.d(TAG, "sensorProps changed: $it") } .stateIn(applicationScope, SharingStarted.Eagerly, null) + private val cameraInfoList: List<CameraInfo> = loadCameraInfoList() + private var currentPhysicalCameraId: String? = null + + private val defaultSensorLocation: StateFlow<Point?> = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : CameraManager.AvailabilityCallback() { + + // This callback will only be called when there is more than one front + // camera on the device (e.g. foldable device with cameras on both outer & + // inner display). + override fun onPhysicalCameraAvailable( + cameraId: String, + physicalCameraId: String + ) { + currentPhysicalCameraId = physicalCameraId + val cameraInfo = + cameraInfoList.firstOrNull { + physicalCameraId == it.cameraPhysicalId + } + trySendWithFailureLogging( + cameraInfo?.cameraLocation, + TAG, + "Update face sensor location to $cameraInfo." + ) + } + + // This callback will only be called when there is more than one front + // camera on the device (e.g. foldable device with cameras on both outer & + // inner display). + // + // By default, all cameras are available which means there will be no + // onPhysicalCameraAvailable() invoked and depending on the device state + // (Fold or unfold), only the onPhysicalCameraUnavailable() for another + // camera will be invoke. So we need to use this method to decide the + // initial physical ID for foldable devices. + override fun onPhysicalCameraUnavailable( + cameraId: String, + physicalCameraId: String + ) { + if (currentPhysicalCameraId == null) { + val cameraInfo = + cameraInfoList.firstOrNull { + physicalCameraId != it.cameraPhysicalId + } + currentPhysicalCameraId = cameraInfo?.cameraPhysicalId + trySendWithFailureLogging( + cameraInfo?.cameraLocation, + TAG, + "Update face sensor location to $cameraInfo." + ) + } + } + } + cameraManager.registerAvailabilityCallback(mainExecutor, callback) + awaitClose { cameraManager.unregisterAvailabilityCallback(callback) } + } + .stateIn( + applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = + if (cameraInfoList.isNotEmpty()) cameraInfoList[0].cameraLocation else null + ) + + override val sensorLocation: StateFlow<Point?> = + sensorInfo + .flatMapLatest { info -> + if (info == null) { + flowOf(null) + } else { + combine( + defaultSensorLocation, + displayStateRepository.currentRotation, + displayStateRepository.currentDisplaySize, + configurationRepository.scaleForResolution + ) { defaultLocation, displayRotation, displaySize, scaleForResolution -> + computeCurrentFaceLocation( + defaultLocation, + displayRotation, + displaySize, + scaleForResolution + ) + } + } + } + .stateIn( + applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + private fun computeCurrentFaceLocation( + defaultLocation: Point?, + rotation: DisplayRotation, + displaySize: Size, + scaleForResolution: Float, + ): Point? { + if (defaultLocation == null) { + return null + } + + return rotateToCurrentOrientation( + Point( + (defaultLocation.x * scaleForResolution).toInt(), + (defaultLocation.y * scaleForResolution).toInt() + ), + rotation, + displaySize + ) + } + + private fun rotateToCurrentOrientation( + inOutPoint: Point, + rotation: DisplayRotation, + displaySize: Size + ): Point { + RotationUtils.rotatePoint( + inOutPoint, + rotation.toRotation(), + displaySize.width, + displaySize.height + ) + return inOutPoint + } override suspend fun getLockoutMode(userId: Int): LockoutMode { if (sensorInfo.value == null || faceManager == null) { return LockoutMode.NONE } return faceManager.getLockoutModeForUser(sensorInfo.value!!.id, userId).toLockoutMode() } + + private fun loadCameraInfoList(): List<CameraInfo> { + val list = mutableListOf<CameraInfo>() + + val outer = + loadCameraInfo( + R.string.config_protectedCameraId, + R.string.config_protectedPhysicalCameraId, + R.array.config_face_auth_props + ) + if (outer != null) { + list.add(outer) + } + + val inner = + loadCameraInfo( + R.string.config_protectedInnerCameraId, + R.string.config_protectedInnerPhysicalCameraId, + R.array.config_inner_face_auth_props + ) + if (inner != null) { + list.add(inner) + } + return list + } + + private fun loadCameraInfo( + cameraIdRes: Int, + cameraPhysicalIdRes: Int, + cameraLocationRes: Int + ): CameraInfo? { + val cameraId = applicationContext.getString(cameraIdRes) + if (cameraId.isNullOrEmpty()) { + return null + } + val physicalCameraId = applicationContext.getString(cameraPhysicalIdRes) + val cameraLocation: IntArray = applicationContext.resources.getIntArray(cameraLocationRes) + val location: Point? + if (cameraLocation.size < 2) { + location = null + } else { + location = Point(cameraLocation[0], cameraLocation[1]) + } + return CameraInfo(cameraId, physicalCameraId, location) + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt new file mode 100644 index 000000000000..cf2e33ce1df5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalMediaModel.kt @@ -0,0 +1,30 @@ +/* + * 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.model + +/** Data model of media on the communal hub. */ +data class CommunalMediaModel( + val hasAnyMediaOrRecommendation: Boolean, + val createdTimestampMillis: Long = 0L, +) { + companion object { + val INACTIVE = + CommunalMediaModel( + hasAnyMediaOrRecommendation = false, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt index e41c32261c11..e8a561b37d20 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt @@ -16,18 +16,17 @@ package com.android.systemui.communal.data.repository +import com.android.systemui.communal.data.model.CommunalMediaModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.pipeline.MediaDataManager import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart /** Encapsulates the state of smartspace in communal. */ interface CommunalMediaRepository { - val mediaPlaying: Flow<Boolean> + val mediaModel: Flow<CommunalMediaModel> } @SysUISingleton @@ -47,27 +46,32 @@ constructor( receivedSmartspaceCardLatency: Int, isSsReactivated: Boolean ) { - if (!mediaDataManager.hasAnyMediaOrRecommendation()) { - return - } - _mediaPlaying.value = true + updateMediaModel(data) } override fun onMediaDataRemoved(key: String) { - if (mediaDataManager.hasAnyMediaOrRecommendation()) { - return - } - _mediaPlaying.value = false + updateMediaModel() } } - private val _mediaPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false) + init { + mediaDataManager.addListener(mediaDataListener) + } - override val mediaPlaying: Flow<Boolean> = - _mediaPlaying - .onStart { - mediaDataManager.addListener(mediaDataListener) - _mediaPlaying.value = mediaDataManager.hasAnyMediaOrRecommendation() - } - .onCompletion { mediaDataManager.removeListener(mediaDataListener) } + private val _mediaModel: MutableStateFlow<CommunalMediaModel> = + MutableStateFlow(CommunalMediaModel.INACTIVE) + + override val mediaModel: Flow<CommunalMediaModel> = _mediaModel + + private fun updateMediaModel(data: MediaData? = null) { + if (mediaDataManager.hasAnyMediaOrRecommendation()) { + _mediaModel.value = + CommunalMediaModel( + hasAnyMediaOrRecommendation = true, + createdTimestampMillis = data?.createdTimestampMillis ?: 0L, + ) + } else { + _mediaModel.value = CommunalMediaModel.INACTIVE + } + } } 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 1f4be4060223..553b3ebc0813 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,6 +56,9 @@ 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) @@ -65,6 +68,9 @@ 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) @@ -96,6 +102,16 @@ 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/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index cab8adfc0bd9..e6816e954b5d 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 @@ -18,14 +18,12 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager -import android.content.BroadcastReceiver import android.content.ComponentName -import android.content.Context 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.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem @@ -40,17 +38,21 @@ 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 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.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow 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 { @@ -58,7 +60,11 @@ interface CommunalWidgetRepository { val communalWidgets: Flow<List<CommunalWidgetContentModel>> /** Add a widget at the specified position in the app widget service and the database. */ - fun addWidget(provider: ComponentName, priority: Int) {} + fun addWidget( + provider: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) {} /** Delete a widget by id from app widget service and the database. */ fun deleteWidget(widgetId: Int) {} @@ -97,37 +103,22 @@ constructor( // Whether the [AppWidgetHost] is listening for updates. private var isHostListening = false - private val isUserUnlocked: Flow<Boolean> = - callbackFlow { - if (!communalRepository.isCommunalEnabled) { - awaitClose() - } + private suspend fun isUserUnlockingOrUnlocked(): Boolean = + withContext(bgDispatcher) { userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) } - fun isUserUnlockingOrUnlocked(): Boolean { - return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) - } - - fun send() { - trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG) - } - - if (isUserUnlockingOrUnlocked()) { - send() - awaitClose() + 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 { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - send() - } - } - - broadcastDispatcher.registerReceiver( - receiver, - IntentFilter(Intent.ACTION_USER_UNLOCKED), - ) - - awaitClose { broadcastDispatcher.unregisterReceiver(receiver) } + emptyFlow() } } .distinctUntilChanged() @@ -148,18 +139,52 @@ constructor( if (!isHostActive || !appWidgetManager.isPresent) { return@flatMapLatest flowOf(emptyList()) } - communalWidgetDao.getWidgets().map { it.map(::mapToContentModel) } + communalWidgetDao + .getWidgets() + .map { it.map(::mapToContentModel) } + // As this reads from a database and triggers IPCs to AppWidgetManager, + // it should be executed in the background. + .flowOn(bgDispatcher) } - override fun addWidget(provider: ComponentName, priority: Int) { + override fun addWidget( + provider: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) { applicationScope.launch(bgDispatcher) { val id = communalWidgetHost.allocateIdAndBindWidget(provider) - id?.let { - communalWidgetDao.addWidget( - widgetId = it, - provider = provider, - priority = priority, - ) + if (id != null) { + val configured = + if (communalWidgetHost.requiresConfiguration(id)) { + logger.i("Widget ${provider.flattenToString()} requires configuration.") + try { + configureWidget.invoke(id) + } catch (ex: Exception) { + // Cleanup the app widget id if an error happens during configuration. + logger.e("Error during widget configuration, cleaning up id $id", ex) + if (ex is CancellationException) { + appWidgetHost.deleteAppWidgetId(id) + // Re-throw cancellation to ensure the parent coroutine also gets + // cancelled. + throw ex + } else { + false + } + } + } else { + logger.i("Skipping configuration for ${provider.flattenToString()}") + true + } + if (configured) { + communalWidgetDao.addWidget( + widgetId = id, + provider = provider, + priority = priority, + ) + } else { + appWidgetHost.deleteAppWidgetId(id) + } } logger.i("Added widget ${provider.flattenToString()} at position $priority.") } @@ -182,6 +207,7 @@ constructor( } } + @WorkerThread private fun mapToContentModel( entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> ): CommunalWidgetContentModel { 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 18fb895f4aaf..24d4c6c4c397 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 @@ -34,15 +34,13 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** Encapsulates business-logic related to communal mode. */ -@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalInteractor @Inject @@ -98,9 +96,20 @@ constructor( editWidgetsActivityStarter.startActivity() } - /** Add a widget at the specified position. */ - fun addWidget(componentName: ComponentName, priority: Int) = - widgetRepository.addWidget(componentName, priority) + /** Dismiss the CTA tile from the hub in view mode. */ + fun dismissCtaTile() = communalRepository.setCtaTileInViewModeVisibility(isVisible = false) + + /** + * Add a widget at the specified position. + * + * @param configureWidget The callback to trigger if widget configuration is needed. Should + * return whether configuration was successful. + */ + fun addWidget( + componentName: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) = widgetRepository.addWidget(componentName, priority, configureWidget) /** Delete a widget by id. */ fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) @@ -125,27 +134,25 @@ constructor( } } - /** A flow of available smartspace content. Currently only showing timer targets. */ - val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> = + /** A flow of available smartspace targets. Currently only showing timers. */ + private val smartspaceTargets: Flow<List<SmartspaceTarget>> = if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) { flowOf(emptyList()) } else { smartspaceRepository.communalSmartspaceTargets.map { targets -> - targets - .filter { target -> - target.featureType == SmartspaceTarget.FEATURE_TIMER && - target.remoteViews != null - } - .mapIndexed Target@{ index, target -> - return@Target CommunalContentModel.Smartspace( - smartspaceTargetId = target.smartspaceTargetId, - remoteViews = target.remoteViews!!, - size = dynamicContentSize(targets.size, index), - ) - } + targets.filter { target -> + target.featureType == SmartspaceTarget.FEATURE_TIMER && + target.remoteViews != null + } } } + /** 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() + } + /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ val tutorialContent: List<CommunalContentModel.Tutorial> = listOf( @@ -159,14 +166,43 @@ constructor( CommunalContentModel.Tutorial(id = 7, HALF), ) - val umoContent: Flow<List<CommunalContentModel.Umo>> = - mediaRepository.mediaPlaying.flatMapLatest { mediaPlaying -> - if (mediaPlaying) { - // TODO(b/310254801): support HALF and FULL layouts - flowOf(listOf(CommunalContentModel.Umo(THIRD))) - } else { - flowOf(emptyList()) + /** + * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and + * sized dynamically. + */ + val ongoingContent: Flow<List<CommunalContentModel.Ongoing>> = + combine(smartspaceTargets, mediaRepository.mediaModel) { smartspace, media -> + val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>() + + // Add smartspace + ongoingContent.addAll( + smartspace.map { target -> + CommunalContentModel.Smartspace( + smartspaceTargetId = target.smartspaceTargetId, + remoteViews = target.remoteViews!!, + createdTimestampMillis = target.creationTimeMillis, + ) + } + ) + + // Add UMO + if (media.hasAnyMediaOrRecommendation) { + ongoingContent.add( + CommunalContentModel.Umo( + createdTimestampMillis = media.createdTimestampMillis, + ) + ) } + + // Order by creation time descending + ongoingContent.sortByDescending { it.createdTimestampMillis } + + // Dynamic sizing + ongoingContent.forEachIndexed { index, model -> + model.size = dynamicContentSize(ongoingContent.size, index) + } + + return@combine ongoingContent } companion object { 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 3ae522970365..46f957f3aaf2 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 @@ -30,46 +30,95 @@ sealed interface CommunalContentModel { /** Size to be rendered in the grid. */ val size: CommunalContentSize + /** + * A type of communal content is ongoing / live / ephemeral, and can be sized and ordered + * dynamically. + */ + sealed interface Ongoing : CommunalContentModel { + override var size: CommunalContentSize + + /** Timestamp in milliseconds of when the content was created. */ + val createdTimestampMillis: Long + } + class Widget( val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, val appWidgetHost: AppWidgetHost, ) : CommunalContentModel { - override val key = "widget_$appWidgetId" + override val key = KEY.widget(appWidgetId) // Widget size is always half. override val size = CommunalContentSize.HALF } /** A placeholder item representing a new widget being added */ class WidgetPlaceholder : CommunalContentModel { - override val key: String = "widget_placeholder_${UUID.randomUUID()}" + override val key: String = KEY.widgetPlaceholder() + // Same as widget size. + override val size = CommunalContentSize.HALF + } + + /** A CTA tile in the glanceable hub view mode which can be dismissed. */ + class CtaTileInViewMode : CommunalContentModel { + override val key: String = KEY.CTA_TILE_IN_VIEW_MODE_KEY + // Same as widget size. + override val size = CommunalContentSize.HALF + } + + /** A CTA tile in the glanceable hub edit model which remains visible in the grid. */ + class CtaTileInEditMode : CommunalContentModel { + override val key: String = KEY.CTA_TILE_IN_EDIT_MODE_KEY // Same as widget size. override val size = CommunalContentSize.HALF } class Tutorial( id: Int, - override val size: CommunalContentSize, + override var size: CommunalContentSize, ) : CommunalContentModel { - override val key = "tutorial_$id" + override val key = KEY.tutorial(id) } class Smartspace( smartspaceTargetId: String, val remoteViews: RemoteViews, - override val size: CommunalContentSize, - ) : CommunalContentModel { - override val key = "smartspace_$smartspaceTargetId" + override val createdTimestampMillis: Long, + override var size: CommunalContentSize = CommunalContentSize.HALF, + ) : Ongoing { + override val key = KEY.smartspace(smartspaceTargetId) } class Umo( - override val size: CommunalContentSize, - ) : CommunalContentModel { - override val key = UMO_KEY + override val createdTimestampMillis: Long, + override var size: CommunalContentSize = CommunalContentSize.HALF, + ) : Ongoing { + override val key = KEY.umo() } - companion object { - /** Key for the [Umo] in CommunalContentModel. There should only ever be one UMO. */ - const val UMO_KEY = "umo" + class KEY { + companion object { + const val CTA_TILE_IN_VIEW_MODE_KEY = "cta_tile_in_view_mode" + const val CTA_TILE_IN_EDIT_MODE_KEY = "cta_tile_in_edit_mode" + + fun widget(id: Int): String { + return "widget_$id" + } + + fun widgetPlaceholder(): String { + return "widget_placeholder_${UUID.randomUUID()}" + } + + fun tutorial(id: Int): String { + return "tutorial_$id" + } + + fun smartspace(id: String): String { + return "smartspace_$id" + } + + fun umo(): String { + return "umo" + } + } } } 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 155de323d3a6..41f9cb4c98ed 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -18,6 +18,8 @@ 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.log.LogBuffer import com.android.systemui.log.core.Logger @@ -63,4 +65,23 @@ constructor( } return false } + + /** + * Returns whether a particular widget requires configuration when it is first added. + * + * Must be called after the widget id has been bound. + */ + fun requiresConfiguration(widgetId: Int): Boolean { + if (appWidgetManager.isPresent) { + val widgetInfo = appWidgetManager.get().getAppWidgetInfo(widgetId) + val featureFlags: Int = widgetInfo.widgetFeatures + // A widget's configuration is optional only if it's configuration is marked as optional + // AND it can be reconfigured later. + val configurationOptional = + (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 && + featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0) + return widgetInfo.configure != null && !configurationOptional + } + return false + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt new file mode 100644 index 000000000000..e167f3e263fe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt @@ -0,0 +1,63 @@ +/* + * 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.shared.log + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger.UiEventEnum + +/** UI events for the Communal Hub. */ +enum class CommunalUiEvent(private val id: Int) : UiEventEnum { + @UiEvent(doc = "Communal Hub is fully shown") COMMUNAL_HUB_SHOWN(1566), + @UiEvent(doc = "Communal Hub starts entering") COMMUNAL_HUB_ENTERING(1575), + @UiEvent(doc = "Communal Hub starts exiting") COMMUNAL_HUB_EXITING(1576), + @UiEvent(doc = "Communal Hub is fully gone") COMMUNAL_HUB_GONE(1577), + @UiEvent(doc = "Communal Hub times out") COMMUNAL_HUB_TIMEOUT(1578), + @UiEvent(doc = "The visible content in the Communal Hub is fully loaded and rendered") + COMMUNAL_HUB_LOADED(1579), + @UiEvent(doc = "User starts the swipe gesture to enter the Communal Hub") + COMMUNAL_HUB_SWIPE_TO_ENTER_START(1580), + @UiEvent(doc = "User finishes the swipe gesture to enter the Communal Hub") + COMMUNAL_HUB_SWIPE_TO_ENTER_FINISH(1581), + @UiEvent(doc = "User cancels the swipe gesture to enter the Communal Hub") + COMMUNAL_HUB_SWIPE_TO_ENTER_CANCEL(1582), + @UiEvent(doc = "User starts the swipe gesture to exit the Communal Hub") + COMMUNAL_HUB_SWIPE_TO_EXIT_START(1583), + @UiEvent(doc = "User finishes the swipe gesture to exit the Communal Hub") + COMMUNAL_HUB_SWIPE_TO_EXIT_FINISH(1584), + @UiEvent(doc = "User cancels the swipe gesture to exit the Communal Hub") + COMMUNAL_HUB_SWIPE_TO_EXIT_CANCEL(1585), + @UiEvent(doc = "User starts the drag gesture to reorder a widget") + COMMUNAL_HUB_REORDER_WIDGET_START(1586), + @UiEvent(doc = "User finishes the drag gesture to reorder a widget") + COMMUNAL_HUB_REORDER_WIDGET_FINISH(1587), + @UiEvent(doc = "User cancels the drag gesture to reorder a widget") + COMMUNAL_HUB_REORDER_WIDGET_CANCEL(1588), + @UiEvent(doc = "Edit mode for the Communal Hub is shown") COMMUNAL_HUB_EDIT_MODE_SHOWN(1569), + @UiEvent(doc = "Edit mode for the Communal Hub is gone") COMMUNAL_HUB_EDIT_MODE_GONE(1589), + @UiEvent(doc = "Widget picker for the Communal Hub is shown") + COMMUNAL_HUB_WIDGET_PICKER_SHOWN(1590), + @UiEvent(doc = "Widget picker for the Communal Hub is gone") + COMMUNAL_HUB_WIDGET_PICKER_GONE(1591), + @UiEvent(doc = "User performs a swipe up gesture from bottom to enter bouncer") + COMMUNAL_HUB_SWIPE_UP_TO_BOUNCER(1573), + @UiEvent(doc = "User performs a swipe down gesture from top to enter shade") + COMMUNAL_HUB_SWIPE_DOWN_TO_SHADE(1574); + + override fun getId(): Int { + return id + } +} 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 c34a8df1bed7..28f48ce1e647 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 @@ -30,6 +30,7 @@ import com.android.systemui.shade.ShadeViewController import javax.inject.Provider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf /** The base view model for the communal hub. */ abstract class BaseCommunalViewModel( @@ -58,8 +59,16 @@ abstract class BaseCommunalViewModel( /** * Called when a widget is added via drag and drop from the widget picker into the communal hub. */ - fun onAddWidget(componentName: ComponentName, priority: Int) { - communalInteractor.addWidget(componentName, priority) + open fun onAddWidget(componentName: ComponentName, priority: Int) { + communalInteractor.addWidget(componentName, priority, ::configureWidget) + } + + /** + * Called when a widget needs to be configured, with the id of the widget. The return value + * should represent whether configuring the widget was successful. + */ + protected open suspend fun configureWidget(widgetId: Int): Boolean { + return true } // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block @@ -88,6 +97,12 @@ abstract class BaseCommunalViewModel( /** Whether in edit mode for the communal hub. */ open val isEditMode = false + /** Whether the popup message triggered by dismissing the CTA tile is showing. */ + open val isPopupOnDismissCtaShowing: Flow<Boolean> = flowOf(false) + + /** Hide the popup message triggered by dismissing the CTA tile. */ + open fun onHidePopupAfterDismissCta() {} + /** Called as the UI requests deleting a widget. */ open fun onDeleteWidget(id: Int) {} @@ -103,6 +118,18 @@ abstract class BaseCommunalViewModel( /** Called as the UI requests opening the widget editor. */ open fun onOpenWidgetEditor() {} + /** Called as the UI requests to dismiss the CTA tile. */ + open fun onDismissCtaTile() {} + /** Gets the interaction handler used to handle taps on a remote view */ abstract fun getInteractionHandler(): RemoteViews.InteractionHandler + + /** Called as the user starts dragging a widget to reorder. */ + open fun onReorderWidgetStart() {} + + /** Called as the user finishes dragging a widget to reorder. */ + open fun onReorderWidgetEnd() {} + + /** Called as the user cancels dragging a widget to reorder. */ + open fun onReorderWidgetCancel() {} } 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 da7bd34950df..0cbf3f1312e2 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 @@ -16,18 +16,31 @@ package com.android.systemui.communal.ui.viewmodel +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.os.PowerManager import android.widget.RemoteViews +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.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import com.android.systemui.shade.ShadeViewController +import com.android.systemui.util.nullableAtomicReference import javax.inject.Inject import javax.inject.Named import javax.inject.Provider +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.map /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -35,16 +48,35 @@ class CommunalEditModeViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, + private val appWidgetHost: AppWidgetHost, shadeViewController: Provider<ShadeViewController>, powerManager: PowerManager, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, + private val uiEventLogger: UiEventLogger, ) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) { + private companion object { + private const val KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle" + private const val SPLASH_SCREEN_STYLE_EMPTY = 0 + } + + private val _widgetsToConfigure = MutableSharedFlow<Int>() + + /** + * Flow emitting ids of widgets which need to be configured. The consumer of this flow should + * trigger [startConfigurationActivity] to initiate configuration. + */ + val widgetsToConfigure: Flow<Int> = _widgetsToConfigure + + private var pendingConfiguration: CompletableDeferred<Int>? by nullableAtomicReference() + override val isEditMode = true - // Only widgets are editable. + // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent + communalInteractor.widgetContent.map { widgets -> + widgets + listOf(CommunalContentModel.CtaTileInEditMode()) + } override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) @@ -55,4 +87,67 @@ constructor( // Ignore all interactions in edit mode. return RemoteViews.InteractionHandler { _, _, _ -> false } } + + override fun onAddWidget(componentName: ComponentName, priority: Int) { + if (pendingConfiguration != null) { + throw IllegalStateException( + "Cannot add $componentName widget while widget configuration is pending" + ) + } + super.onAddWidget(componentName, priority) + } + + fun startConfigurationActivity(activity: Activity, widgetId: Int, requestCode: Int) { + val options = + ActivityOptions.makeBasic().apply { + setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + } + val bundle = options.toBundle() + bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY) + try { + appWidgetHost.startAppWidgetConfigureActivityForResult( + activity, + widgetId, + 0, + // Use the widget id as the request code. + requestCode, + bundle + ) + } catch (e: ActivityNotFoundException) { + setConfigurationResult(RESULT_CANCELED) + } + } + + override suspend fun configureWidget(widgetId: Int): Boolean { + if (pendingConfiguration != null) { + throw IllegalStateException( + "Attempting to configure $widgetId while another configuration is already active" + ) + } + pendingConfiguration = CompletableDeferred() + _widgetsToConfigure.emit(widgetId) + val resultCode = pendingConfiguration?.await() ?: RESULT_CANCELED + pendingConfiguration = null + return resultCode == RESULT_OK + } + + /** Sets the result of widget configuration. */ + fun setConfigurationResult(resultCode: Int) { + pendingConfiguration?.complete(resultCode) + ?: throw IllegalStateException("No widget pending configuration") + } + + override fun onReorderWidgetStart() { + uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) + } + + override fun onReorderWidgetEnd() { + uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) + } + + override fun onReorderWidgetCancel() { + 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 2fae8b533857..1c696851bb70 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 @@ -23,23 +23,31 @@ import com.android.systemui.communal.domain.interactor.CommunalTutorialInteracto import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import com.android.systemui.shade.ShadeViewController import javax.inject.Inject import javax.inject.Named import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch /** The default view model used for showing the communal hub. */ @SysUISingleton class CommunalViewModel @Inject constructor( + @Application private val scope: CoroutineScope, private val communalInteractor: CommunalInteractor, private val interactionHandler: WidgetInteractionHandler, tutorialInteractor: CommunalTutorialInteractor, @@ -54,15 +62,54 @@ constructor( return@flatMapLatest flowOf(communalInteractor.tutorialContent) } combine( - communalInteractor.smartspaceContent, - communalInteractor.umoContent, + communalInteractor.ongoingContent, communalInteractor.widgetContent, - ) { smartspace, umo, widgets -> - smartspace + umo + widgets + communalInteractor.ctaTileContent, + ) { ongoing, widgets, ctaTile, + -> + ongoing + widgets + ctaTile } } + private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isPopupOnDismissCtaShowing: Flow<Boolean> = + _isPopupOnDismissCtaShowing.asStateFlow() + override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() + override fun onDismissCtaTile() { + communalInteractor.dismissCtaTile() + setPopupOnDismissCtaVisibility(true) + schedulePopupHiding() + } + override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler + + override fun onHidePopupAfterDismissCta() { + cancelDelayedPopupHiding() + setPopupOnDismissCtaVisibility(false) + } + + private fun setPopupOnDismissCtaVisibility(isVisible: Boolean) { + _isPopupOnDismissCtaShowing.value = isVisible + } + + private var delayedHidePopupJob: Job? = null + private fun schedulePopupHiding() { + cancelDelayedPopupHiding() + delayedHidePopupJob = + scope.launch { + delay(POPUP_AUTO_HIDE_TIMEOUT_MS) + onHidePopupAfterDismissCta() + } + } + + private fun cancelDelayedPopupHiding() { + delayedHidePopupJob?.cancel() + delayedHidePopupJob = null + } + + companion object { + const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 0f94a92dd7ce..bfc6f2b14acd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -27,23 +27,26 @@ import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import com.android.systemui.communal.domain.interactor.CommunalInteractor +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent import javax.inject.Inject +import kotlinx.coroutines.launch /** An Activity for editing the widgets that appear in hub mode. */ class EditWidgetsActivity @Inject constructor( private val communalViewModel: CommunalEditModeViewModel, - private val communalInteractor: CommunalInteractor, private var windowManagerService: IWindowManager? = null, ) : ComponentActivity() { companion object { private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" private const val EXTRA_FILTER_STRATEGY = "filter_strategy" private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1 + private const val REQUEST_CODE_CONFIGURE_WIDGET = 1 private const val TAG = "EditWidgetsActivity" } @@ -63,7 +66,7 @@ constructor( Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java ) - ?.let { communalInteractor.addWidget(it, 0) } + ?.let { communalViewModel.onAddWidget(it, 0) } ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } } } @@ -84,14 +87,26 @@ constructor( windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // Start the configuration activity when new widgets are added. + communalViewModel.widgetsToConfigure.collect { widgetId -> + communalViewModel.startConfigurationActivity( + activity = this@EditWidgetsActivity, + widgetId = widgetId, + requestCode = REQUEST_CODE_CONFIGURE_WIDGET + ) + } + } + } + setCommunalEditWidgetActivityContent( activity = this, viewModel = communalViewModel, onOpenWidgetPicker = { - val localPackageManager: PackageManager = getPackageManager() val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } - localPackageManager + packageManager .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ?.activityInfo ?.packageName @@ -122,4 +137,11 @@ constructor( } ) } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_CONFIGURE_WIDGET) { + communalViewModel.setConfigurationResult(resultCode) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 8b992fcae67e..b2d70523c282 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -91,6 +91,7 @@ import android.telecom.TelecomManager; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.telephony.satellite.SatelliteManager; import android.view.Choreographer; import android.view.CrossWindowBlurListeners; import android.view.IWindowManager; @@ -712,4 +713,10 @@ public class FrameworkServicesModule { ServiceManager.getService(Context.URI_GRANTS_SERVICE) ); } + + @Provides + @Singleton + static Optional<SatelliteManager> provideSatelliteManager(Context context) { + return Optional.ofNullable(context.getSystemService(SatelliteManager.class)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index 615b503b9fae..3bc4f342c566 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -32,6 +32,7 @@ import android.widget.FrameLayout import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.ScreenDecorationsLogger @@ -41,19 +42,20 @@ import javax.inject.Inject @SysUISingleton class FaceScanningProviderFactory @Inject constructor( - private val authController: AuthController, - private val context: Context, - private val statusBarStateController: StatusBarStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - @Main private val mainExecutor: Executor, - private val logger: ScreenDecorationsLogger, + private val authController: AuthController, + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + @Main private val mainExecutor: Executor, + private val logger: ScreenDecorationsLogger, + private val facePropertyRepository: FacePropertyRepository, ) : DecorProviderFactory() { private val display = context.display private val displayInfo = DisplayInfo() override val hasProviders: Boolean get() { - if (authController.faceSensorLocation == null) { + if (facePropertyRepository.sensorLocation.value == null) { return false } @@ -86,6 +88,7 @@ class FaceScanningProviderFactory @Inject constructor( keyguardUpdateMonitor, mainExecutor, logger, + facePropertyRepository, ) ) } @@ -104,12 +107,13 @@ class FaceScanningProviderFactory @Inject constructor( } class FaceScanningOverlayProviderImpl( - override val alignedBound: Int, - private val authController: AuthController, - private val statusBarStateController: StatusBarStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val mainExecutor: Executor, - private val logger: ScreenDecorationsLogger, + override val alignedBound: Int, + private val authController: AuthController, + private val statusBarStateController: StatusBarStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mainExecutor: Executor, + private val logger: ScreenDecorationsLogger, + private val facePropertyRepository: FacePropertyRepository, ) : BoundDecorProvider() { override val viewId: Int = com.android.systemui.res.R.id.face_scanning_anim @@ -162,8 +166,9 @@ class FaceScanningOverlayProviderImpl( layoutParams.let { lp -> lp.width = ViewGroup.LayoutParams.MATCH_PARENT lp.height = ViewGroup.LayoutParams.MATCH_PARENT - logger.faceSensorLocation(authController.faceSensorLocation) - authController.faceSensorLocation?.y?.let { faceAuthSensorHeight -> + logger.faceSensorLocation(facePropertyRepository.sensorLocation.value) + facePropertyRepository.sensorLocation.value?.y?.let { + faceAuthSensorHeight -> val faceScanningHeight = (faceAuthSensorHeight * 2) when (rotation) { Surface.ROTATION_0, Surface.ROTATION_180 -> diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 38c7c6ac67cb..699532cbfca3 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -448,11 +448,6 @@ object Flags { // TODO(b/270987164): Tracking Bug @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag("trackpad_gesture_features") - // TODO(b/265639042): Tracking Bug - @JvmField - val WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM = - unreleasedFlag("persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true) - // TODO(b/273800936): Tracking Bug @JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag("trackpad_gesture_common") 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 2f937bcd3414..704ebdd40af6 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 @@ -21,6 +21,7 @@ import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.Position @@ -277,6 +278,7 @@ constructor( @Main private val mainDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, private val systemClock: SystemClock, + facePropertyRepository: FacePropertyRepository, ) : KeyguardRepository { private val _dismissAction: MutableStateFlow<DismissAction> = MutableStateFlow(DismissAction.None) @@ -599,27 +601,7 @@ constructor( awaitClose { authController.removeCallback(callback) } } - override val faceSensorLocation: Flow<Point?> = conflatedCallbackFlow { - fun sendSensorLocation() { - trySendWithFailureLogging( - authController.faceSensorLocation, - TAG, - "AuthController.Callback#onFingerprintLocationChanged" - ) - } - - val callback = - object : AuthController.Callback { - override fun onFaceSensorLocationChanged() { - sendSensorLocation() - } - } - - authController.addCallback(callback) - sendSensorLocation() - - awaitClose { authController.removeCallback(callback) } - } + override val faceSensorLocation: Flow<Point?> = facePropertyRepository.sensorLocation override val biometricUnlockSource: Flow<BiometricUnlockSource?> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index b98e9c232d58..5caa27f02bd3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -81,9 +81,12 @@ data class MediaData( /** Set from the notification and used as fallback when PlaybackState cannot be determined */ val isClearable: Boolean = true, - /** Timestamp when this player was last active. */ + /** Milliseconds since boot when this player was last active. */ var lastActive: Long = 0L, + /** Timestamp in milliseconds when this player was created. */ + var createdTimestampMillis: Long = 0L, + /** Instance ID for logging purposes */ val instanceId: InstanceId, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 3e8b49d4a653..47df3b79b8bb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -400,7 +400,12 @@ class MediaDataManager( val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { val instanceId = logger.getNewInstanceId() - val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId) + val temp = + LOADING.copy( + packageName = sbn.packageName, + instanceId = instanceId, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) mediaEntries.put(key, temp) isNewlyActiveEntry = true } else if (oldKey != key) { @@ -454,7 +459,8 @@ class MediaDataManager( resumeAction = action, hasCheckedForResume = true, instanceId = instanceId, - appUid = appUid + appUid = appUid, + createdTimestampMillis = systemClock.currentTimeMillis(), ) mediaEntries.put(packageName, resumeData) logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) @@ -732,6 +738,7 @@ class MediaDataManager( val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L foregroundExecutor.execute { onMediaDataLoaded( packageName, @@ -757,6 +764,7 @@ class MediaDataManager( notificationKey = packageName, hasCheckedForResume = true, lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, instanceId = instanceId, appUid = appUid, isExplicit = isExplicit, @@ -907,6 +915,7 @@ class MediaDataManager( } val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true @@ -937,6 +946,7 @@ class MediaDataManager( isPlaying = isPlaying, isClearable = !sbn.isOngoing, lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, instanceId = instanceId, appUid = appUid, isExplicit = isExplicit, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index 6ec46f627264..df6843d31ab1 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -61,6 +61,7 @@ import com.android.systemui.Dependency; import com.android.systemui.assist.AssistManager; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.res.R; +import com.android.systemui.shared.navigationbar.KeyButtonRipple; import com.android.systemui.shared.system.QuickStepContract; public class KeyButtonView extends ImageView implements ButtonInterface { 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 e660b97b5c6b..0d641ac9c688 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -758,7 +758,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void updateMLModelState() { - boolean newState = mIsGestureHandlingEnabled && DeviceConfig.getBoolean( + boolean newState = mIsGestureHandlingEnabled && mContext.getResources().getBoolean( + R.bool.config_useBackGestureML) && DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index ddd7d6781c46..51b94dd983f3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -189,6 +189,7 @@ public class QSPanel extends LinearLayout implements Tunable { public void setBrightnessView(@NonNull View view) { if (mBrightnessView != null) { removeView(mBrightnessView); + mChildrenLayoutTop.remove(mBrightnessView); mMovableContentStartIndex--; } addView(view, 0); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index 5eb9620d7334..ef58a608aa1f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -56,14 +56,18 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { private final QSCustomizerController mQsCustomizerController; private final QSTileRevealController.Factory mQsTileRevealControllerFactory; private final FalsingManager mFalsingManager; - private final BrightnessController mBrightnessController; - private final BrightnessSliderController mBrightnessSliderController; - private final BrightnessMirrorHandler mBrightnessMirrorHandler; + private BrightnessController mBrightnessController; + private BrightnessSliderController mBrightnessSliderController; + private BrightnessMirrorHandler mBrightnessMirrorHandler; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private boolean mListening; private final boolean mSceneContainerEnabled; + private int mLastDensity; + private final BrightnessSliderController.Factory mBrightnessSliderControllerFactory; + private final BrightnessController.Factory mBrightnessControllerFactory; + private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -93,6 +97,8 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mQsCustomizerController = qsCustomizerController; mQsTileRevealControllerFactory = qsTileRevealControllerFactory; mFalsingManager = falsingManager; + mBrightnessSliderControllerFactory = brightnessSliderFactory; + mBrightnessControllerFactory = brightnessControllerFactory; mBrightnessSliderController = brightnessSliderFactory.create(getContext(), mView); mView.setBrightnessView(mBrightnessSliderController.getRootView()); @@ -100,6 +106,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController); mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController); mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; + mLastDensity = view.getResources().getConfiguration().densityDpi; mSceneContainerEnabled = sceneContainerFlags.isEnabled(); } @@ -147,11 +154,31 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { @Override protected void onConfigurationChanged() { mView.updateResources(); + int newDensity = mView.getResources().getConfiguration().densityDpi; + if (newDensity != mLastDensity) { + mLastDensity = newDensity; + reinflateBrightnessSlider(); + } + if (mView.isListening()) { refreshAllTiles(); } } + private void reinflateBrightnessSlider() { + mBrightnessController.unregisterCallbacks(); + mBrightnessSliderController = + mBrightnessSliderControllerFactory.create(getContext(), mView); + mView.setBrightnessView(mBrightnessSliderController.getRootView()); + mBrightnessController = mBrightnessControllerFactory.create(mBrightnessSliderController); + mBrightnessMirrorHandler.setBrightnessController(mBrightnessController); + mBrightnessSliderController.init(); + if (mListening) { + mBrightnessController.registerCallbacks(); + } + } + + @Override protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) { ((PagedTileLayout) mView.getOrCreateTileLayout()) diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java index 36dc7433df0a..a01d65896592 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java @@ -67,9 +67,7 @@ public class ReduceBrightColorsController implements synchronized (mListeners) { if (setting != null && mListeners.size() != 0) { if (setting.equals(Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED)) { - for (Listener listener : mListeners) { - listener.onActivated(mManager.isReduceBrightColorsActivated()); - } + dispatchOnActivated(mManager.isReduceBrightColorsActivated()); } } } @@ -125,6 +123,13 @@ public class ReduceBrightColorsController implements mManager.setReduceBrightColorsActivated(activated); } + private void dispatchOnActivated(boolean activated) { + ArrayList<Listener> copy = new ArrayList<>(mListeners); + for (Listener l : copy) { + l.onActivated(activated); + } + } + /** * Listener invoked whenever the Reduce Bright Colors settings are changed. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt index e075e76595d4..2b8c335cb0ad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt @@ -24,6 +24,7 @@ import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R +import com.android.systemui.util.time.SystemClock import java.time.Instant import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -36,10 +37,12 @@ class AlarmTileMapper constructor( @Main private val resources: Resources, private val theme: Theme, + private val clock: SystemClock, ) : QSTileDataToStateMapper<AlarmTileModel> { companion object { val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("E hh:mm a") val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("E HH:mm") + val formatterDateOnly: DateTimeFormatter = DateTimeFormatter.ofPattern("E MMM d") } override fun map(config: QSTileConfig, data: AlarmTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { @@ -47,14 +50,32 @@ constructor( is AlarmTileModel.NextAlarmSet -> { activationState = QSTileState.ActivationState.ACTIVE - val localDateTime = + val alarmDateTime = LocalDateTime.ofInstant( Instant.ofEpochMilli(data.alarmClockInfo.triggerTime), TimeZone.getDefault().toZoneId() ) - secondaryLabel = - if (data.is24HourFormat) formatter24Hour.format(localDateTime) - else formatter12Hour.format(localDateTime) + + val nowDateTime = + LocalDateTime.ofInstant( + Instant.ofEpochMilli(clock.currentTimeMillis()), + TimeZone.getDefault().toZoneId() + ) + + // Edge case: If it's 8:00:30 right now and alarm is requested for next week at + // 8:00:29, we still want to show the date. Same at nanosecond level. + val nextWeekThisTime = nowDateTime.plusWeeks(1).withSecond(0).withNano(0) + + // is the alarm over a week away? + val shouldShowDateAndHideTime = alarmDateTime >= nextWeekThisTime + + if (shouldShowDateAndHideTime) { + secondaryLabel = formatterDateOnly.format(alarmDateTime) + } else { + secondaryLabel = + if (data.is24HourFormat) formatter24Hour.format(alarmDateTime) + else formatter12Hour.format(alarmDateTime) + } } is AlarmTileModel.NoAlarmSet -> { activationState = QSTileState.ActivationState.INACTIVE diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt index 51aa339149a4..701d814a843b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt @@ -19,9 +19,16 @@ package com.android.systemui.settings.brightness import com.android.systemui.statusbar.policy.BrightnessMirrorController import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener -class BrightnessMirrorHandler(private val brightnessController: MirroredBrightnessController) { +class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController) { - private var mirrorController: BrightnessMirrorController? = null + var mirrorController: BrightnessMirrorController? = null + private set + + var brightnessController: MirroredBrightnessController = brightnessController + set(value) { + field = value + updateBrightnessMirror() + } private val brightnessMirrorListener = BrightnessMirrorListener { updateBrightnessMirror() } @@ -33,7 +40,7 @@ class BrightnessMirrorHandler(private val brightnessController: MirroredBrightne mirrorController?.removeCallback(brightnessMirrorListener) } - fun setController(controller: BrightnessMirrorController) { + fun setController(controller: BrightnessMirrorController?) { mirrorController?.removeCallback(brightnessMirrorListener) mirrorController = controller mirrorController?.addCallback(brightnessMirrorListener) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index a5c1cf1af191..6f4a1e7754f5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -4362,8 +4362,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onHeadsUpPinned(NotificationEntry entry) { if (!isKeyguardShowing()) { - mNotificationStackScrollLayoutController.generateHeadsUpAnimation( - entry.getHeadsUpAnimationView(), true); + mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true); } } @@ -4375,8 +4374,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // notification // will stick to the top without any interaction. if (isFullyCollapsed() && entry.isRowHeadsUp() && !isKeyguardShowing()) { - mNotificationStackScrollLayoutController.generateHeadsUpAnimation( - entry.getHeadsUpAnimationView(), false); + mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, false); entry.setHeadsUpIsVisible(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index a957095536eb..32cd56ca223f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.dagger; +import static com.android.systemui.Flags.predictiveBackAnimateDialogs; + import android.content.Context; import android.os.RemoteException; import android.service.dreams.IDreamManager; @@ -31,8 +33,6 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.settings.DisplayTracker; @@ -230,11 +230,11 @@ public interface CentralSurfacesDependenciesModule { /** */ @Provides @SysUISingleton - static AnimationFeatureFlags provideAnimationFeatureFlags(FeatureFlags featureFlags) { + static AnimationFeatureFlags provideAnimationFeatureFlags() { return new AnimationFeatureFlags() { @Override public boolean isPredictiveBackQsDialogAnim() { - return featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM); + return predictiveBackAnimateDialogs(); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 0c67279c1660..3f2c818399d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -31,6 +31,7 @@ import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.StackStateAnimator @@ -58,6 +59,7 @@ constructor( private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController, private val logger: NotificationWakeUpCoordinatorLogger, + private val notifsKeyguardInteractor: NotificationsKeyguardInteractor, ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, @@ -144,6 +146,7 @@ constructor( for (listener in wakeUpListeners) { listener.onFullyHiddenChanged(value) } + notifsKeyguardInteractor.setNotificationsFullyHidden(value) } } @@ -216,6 +219,7 @@ constructor( for (listener in wakeUpListeners) { listener.onPulseExpandingChanged(pulseExpanding) } + notifsKeyguardInteractor.setPulseExpanding(pulseExpanding) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt index 5435fb5449cd..2cac0002f013 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt @@ -15,8 +15,6 @@ */ package com.android.systemui.statusbar.notification.data -import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardStateRepositoryModule import dagger.Module -@Module(includes = [NotificationsKeyguardStateRepositoryModule::class]) -interface NotificationDataLayerModule +@Module(includes = []) interface NotificationDataLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt index 2cc1403a80a5..bd6ea30c44e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt @@ -15,59 +15,16 @@ */ package com.android.systemui.statusbar.notification.data.repository -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator -import dagger.Binds -import dagger.Module import javax.inject.Inject -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow /** View-states pertaining to notifications on the keyguard. */ -interface NotificationsKeyguardViewStateRepository { +@SysUISingleton +class NotificationsKeyguardViewStateRepository @Inject constructor() { /** Are notifications fully hidden from view? */ - val areNotificationsFullyHidden: Flow<Boolean> + val areNotificationsFullyHidden = MutableStateFlow(false) /** Is a pulse expansion occurring? */ - val isPulseExpanding: Flow<Boolean> -} - -@Module -interface NotificationsKeyguardStateRepositoryModule { - @Binds - fun bindImpl( - impl: NotificationsKeyguardViewStateRepositoryImpl - ): NotificationsKeyguardViewStateRepository -} - -@SysUISingleton -class NotificationsKeyguardViewStateRepositoryImpl -@Inject -constructor( - wakeUpCoordinator: NotificationWakeUpCoordinator, -) : NotificationsKeyguardViewStateRepository { - override val areNotificationsFullyHidden: Flow<Boolean> = conflatedCallbackFlow { - val listener = - object : NotificationWakeUpCoordinator.WakeUpListener { - override fun onFullyHiddenChanged(isFullyHidden: Boolean) { - trySend(isFullyHidden) - } - } - trySend(wakeUpCoordinator.notificationsFullyHidden) - wakeUpCoordinator.addListener(listener) - awaitClose { wakeUpCoordinator.removeListener(listener) } - } - - override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow { - val listener = - object : NotificationWakeUpCoordinator.WakeUpListener { - override fun onPulseExpandingChanged(isPulseExpanding: Boolean) { - trySend(isPulseExpanding) - } - } - trySend(wakeUpCoordinator.isPulseExpanding()) - wakeUpCoordinator.addListener(listener) - awaitClose { wakeUpCoordinator.removeListener(listener) } - } + val isPulseExpanding = MutableStateFlow(false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt index 73341dbc4999..a6361cbc9f9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt @@ -15,24 +15,29 @@ */ package com.android.systemui.statusbar.notification.domain.interactor -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn /** Domain logic pertaining to notifications on the keyguard. */ class NotificationsKeyguardInteractor @Inject constructor( - repository: NotificationsKeyguardViewStateRepository, - @Background backgroundDispatcher: CoroutineDispatcher, + private val repository: NotificationsKeyguardViewStateRepository, ) { /** Is a pulse expansion occurring? */ - val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding.flowOn(backgroundDispatcher) + val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding /** Are notifications fully hidden from view? */ - val areNotificationsFullyHidden: Flow<Boolean> = - repository.areNotificationsFullyHidden.flowOn(backgroundDispatcher) + val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden + + /** Updates whether notifications are fully hidden from view. */ + fun setNotificationsFullyHidden(fullyHidden: Boolean) { + repository.areNotificationsFullyHidden.value = fullyHidden + } + + /** Updates whether a pulse expansion is occurring. */ + fun setPulseExpanding(pulseExpanding: Boolean) { + repository.isPulseExpanding.value = pulseExpanding + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index f6431a2e65ad..7ea9b14353d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -32,6 +32,8 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.util.ContrastColorUtil; +import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.res.R; @@ -58,11 +60,19 @@ public class NotificationBackgroundView extends View implements Dumpable { private int mExpandAnimationWidth = -1; private int mExpandAnimationHeight = -1; private int mDrawableAlpha = 255; + private final ColorStateList mLightColoredStatefulColors; + private final ColorStateList mDarkColoredStatefulColors; + private final int mNormalColor; public NotificationBackgroundView(Context context, AttributeSet attrs) { super(context, attrs); - mDontModifyCorners = getResources().getBoolean( - R.bool.config_clipNotificationsToOutline); + mDontModifyCorners = getResources().getBoolean(R.bool.config_clipNotificationsToOutline); + mLightColoredStatefulColors = getResources().getColorStateList( + R.color.notification_state_color_light); + mDarkColoredStatefulColors = getResources().getColorStateList( + R.color.notification_state_color_dark); + mNormalColor = Utils.getColorAttrDefaultColor(mContext, + com.android.internal.R.attr.materialColorSurfaceContainerHigh); } @Override @@ -122,6 +132,18 @@ public class NotificationBackgroundView extends View implements Dumpable { } /** + * Stateful colors are colors that will overlay on the notification original color when one of + * hover states, pressed states or other similar states is activated. + */ + private void setStatefulColors() { + if (mTintColor != mNormalColor) { + ColorStateList newColor = ContrastColorUtil.isColorDark(mTintColor) + ? mDarkColoredStatefulColors : mLightColoredStatefulColors; + ((GradientDrawable) getStatefulBackgroundLayer().mutate()).setColor(newColor); + } + } + + /** * Sets a background drawable. As we need to change our bounds independently of layout, we need * the notion of a background independently of the regular View background.. */ @@ -149,21 +171,20 @@ public class NotificationBackgroundView extends View implements Dumpable { setCustomBackground(d); } + private Drawable getBaseBackgroundLayer() { + return ((LayerDrawable) mBackground).getDrawable(0); + } + + private Drawable getStatefulBackgroundLayer() { + return ((LayerDrawable) mBackground).getDrawable(1); + } + public void setTint(int tintColor) { - if (tintColor != 0) { - ColorStateList stateList = new ColorStateList(new int[][]{ - new int[]{com.android.internal.R.attr.state_pressed}, - new int[]{com.android.internal.R.attr.state_hovered}, - new int[]{}}, - - new int[]{tintColor, 0, tintColor} - ); - mBackground.setTintMode(PorterDuff.Mode.SRC_ATOP); - mBackground.setTintList(stateList); - } else { - mBackground.setTintList(null); - } + Drawable baseLayer = getBaseBackgroundLayer(); + baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP); + baseLayer.setTint(tintColor); mTintColor = tintColor; + setStatefulColors(); invalidate(); } 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 805b44cc0673..ea414d2c78d0 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 @@ -4869,10 +4869,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { ExpandableNotificationRow row = entry.getHeadsUpAnimationView(); - generateHeadsUpAnimation(row, isHeadsUp); - } - - public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation:" 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 2e545126c634..abc04b87f831 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 @@ -1495,14 +1495,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView.getFirstChildNotGone(); } - private void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { + public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { mView.generateHeadsUpAnimation(entry, isHeadsUp); } - public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { - mView.generateHeadsUpAnimation(row, isHeadsUp); - } - public void setMaxTopPadding(int padding) { mView.setMaxTopPadding(padding); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt index 38a6d39b04e2..13d7924a8be6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ComponentSystemUIDialog.kt @@ -34,7 +34,6 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.flags.FeatureFlags import com.android.systemui.model.SysUiState /** @@ -53,7 +52,6 @@ class ComponentSystemUIDialog( context: Context, theme: Int, dismissOnDeviceLock: Boolean, - featureFlags: FeatureFlags, dialogManager: SystemUIDialogManager, sysUiState: SysUiState, broadcastDispatcher: BroadcastDispatcher, @@ -63,7 +61,6 @@ class ComponentSystemUIDialog( context, theme, dismissOnDeviceLock, - featureFlags, dialogManager, sysUiState, broadcastDispatcher, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index abdf8277e0c9..56ea00cf0954 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -104,7 +104,8 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { } private void notifyManagedProfileRemoved() { - for (Callback callback : mCallbacks) { + ArrayList<Callback> copy = new ArrayList<>(mCallbacks); + for (Callback callback : copy) { callback.onManagedProfileRemoved(); } } @@ -148,7 +149,8 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { @Override public void onUserChanged(int newUser, @NonNull Context userContext) { reloadManagedProfiles(); - for (Callback callback : mCallbacks) { + ArrayList<Callback> copy = new ArrayList<>(mCallbacks); + for (Callback callback : copy) { callback.onManagedProfileChanged(); } } @@ -156,7 +158,8 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { @Override public void onProfilesChanged(@NonNull List<UserInfo> profiles) { reloadManagedProfiles(); - for (Callback callback : mCallbacks) { + ArrayList<Callback> copy = new ArrayList<>(mCallbacks); + for (Callback callback : copy) { callback.onManagedProfileChanged(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 9ae41951bb74..d7cbe5df419b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -14,6 +14,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_BINDABLE; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW; @@ -40,11 +41,13 @@ import com.android.systemui.statusbar.BaseStatusBarFrameLayout; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; @@ -432,6 +435,10 @@ public interface StatusBarIconController { case TYPE_MOBILE_NEW: return addNewMobileIcon(index, slot, holder.getTag()); + + case TYPE_BINDABLE: + // Safe cast, since only BindableIconHolders can set this tag on themselves + return addBindableIcon((BindableIconHolder) holder, index); } return null; @@ -446,6 +453,18 @@ public interface StatusBarIconController { return view; } + /** + * ModernStatusBarViews can be created and bound, and thus do not need to update their + * drawable by sending multiple calls to setIcon. Instead, by using a bindable + * icon view, we can simply create the icon when requested and allow the + * ViewBinder to control its visual state. + */ + protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, int index) { + ModernStatusBarView view = holder.getInitializer().createAndBind(mContext); + mGroup.addView(view, index, onCreateLayoutParams()); + return view; + } + protected StatusIconDisplayable addNewWifiIcon(int index, String slot) { ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); mGroup.addView(view, index, onCreateLayoutParams()); @@ -530,6 +549,7 @@ public interface StatusBarIconController { return; case TYPE_MOBILE_NEW: case TYPE_WIFI_NEW: + case TYPE_BINDABLE: // Nothing, the new icons update themselves return; default: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 0f4d68c68d00..4f148f112c52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -38,8 +38,11 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusIconDisplayable; +import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry; +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.tuner.TunerService; @@ -83,7 +86,8 @@ public class StatusBarIconControllerImpl implements Tunable, TunerService tunerService, DumpManager dumpManager, StatusBarIconList statusBarIconList, - StatusBarPipelineFlags statusBarPipelineFlags + StatusBarPipelineFlags statusBarPipelineFlags, + BindableIconsRegistry modernIconsRegistry ) { mStatusBarIconList = statusBarIconList; mContext = context; @@ -94,6 +98,28 @@ public class StatusBarIconControllerImpl implements Tunable, tunerService.addTunable(this, ICON_HIDE_LIST); demoModeController.addCallback(this); dumpManager.registerDumpable(getClass().getSimpleName(), this); + + addModernBindableIcons(modernIconsRegistry); + } + + /** + * BindableIcons will always produce ModernStatusBarViews, which will be initialized and bound + * upon being added to any icon group. Because their view policy does not require subsequent + * calls to setIcon(), we can simply register them all statically here and not have to build + * CoreStartables for each modern icon. + * + * @param registry a statically defined provider of the modern icons + */ + private void addModernBindableIcons(BindableIconsRegistry registry) { + List<BindableIcon> icons = registry.getBindableIcons(); + + // Initialization point for the bindable (modern) icons. These icons get their own slot + // allocated immediately, and are required to control their own display properties + for (BindableIcon i : icons) { + if (i.getShouldBindIcon()) { + addBindableIcon(i); + } + } } /** */ @@ -182,6 +208,17 @@ public class StatusBarIconControllerImpl implements Tunable, mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder)); } + void addBindableIcon(BindableIcon icon) { + StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0); + // Expected to be null + if (existingHolder == null) { + BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer()); + setIcon(icon.getSlot(), bindableIcon); + } else { + Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring"); + } + } + /** */ @Override public void setIcon(String slot, int resourceId, CharSequence contentDescription) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt index 5b55a1e73dc3..bef0b286ebf8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt @@ -21,23 +21,24 @@ import android.graphics.drawable.Icon import android.os.UserHandle import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator /** Wraps [com.android.internal.statusbar.StatusBarIcon] so we can still have a uniform list */ -class StatusBarIconHolder private constructor() { - @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW) +open class StatusBarIconHolder private constructor() { + @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW, TYPE_BINDABLE) @Retention(AnnotationRetention.SOURCE) internal annotation class IconType var icon: StatusBarIcon? = null @IconType - var type = TYPE_ICON - private set + open var type = TYPE_ICON + internal set var tag = 0 private set - var isVisible: Boolean + open var isVisible: Boolean get() = when (type) { TYPE_ICON -> icon!!.visible @@ -45,6 +46,7 @@ class StatusBarIconHolder private constructor() { // The new pipeline controls visibilities via the view model and // view binder, so // this is effectively an unused return value. + TYPE_BINDABLE, TYPE_MOBILE_NEW, TYPE_WIFI_NEW -> true else -> true @@ -55,6 +57,7 @@ class StatusBarIconHolder private constructor() { } when (type) { TYPE_ICON -> icon!!.visible = visible + TYPE_BINDABLE, TYPE_MOBILE_NEW, TYPE_WIFI_NEW -> {} } @@ -94,6 +97,9 @@ class StatusBarIconHolder private constructor() { ) const val TYPE_WIFI_NEW = 4 + /** Only applicable to [BindableIconHolder] */ + const val TYPE_BINDABLE = 5 + /** Returns a human-readable string representing the given type. */ fun getTypeString(@IconType type: Int): String { return when (type) { @@ -154,4 +160,25 @@ class StatusBarIconHolder private constructor() { return holder } } + + /** + * Subclass of StatusBarIconHolder that is responsible only for the registration of an icon into + * the [StatusBarIconList]. A bindable icon takes care of its own display, including hiding + * itself under the correct conditions. + * + * StatusBarIconController will register all available bindable icons on init (see + * [BindableIconsRepository]), and will ignore any call to setIcon for these. + * + * [initializer] a view creator that can bind the relevant view models to the created view. + */ + class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() { + override var type: Int = TYPE_BINDABLE + + /** This is unused, as bindable icons use their own view binders to control visibility */ + override var isVisible: Boolean = true + + override fun toString(): String { + return ("StatusBarIconHolder(type=BINDABLE)") + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 3394eacddbd8..390d2c973882 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.Flags.predictiveBackAnimateDialogs; + import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; @@ -45,8 +47,6 @@ import com.android.systemui.Dependency; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.model.SysUiState; import com.android.systemui.res.R; import com.android.systemui.shared.system.QuickStepContract; @@ -78,7 +78,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh public static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true; private final Context mContext; - private final FeatureFlags mFeatureFlags; private final DialogDelegate<SystemUIDialog> mDelegate; @Nullable private final DismissReceiver mDismissReceiver; private final Handler mHandler = new Handler(); @@ -110,7 +109,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set // the content and attach listeners. this(context, theme, dismissOnDeviceLock, - Dependency.get(FeatureFlags.class), Dependency.get(SystemUIDialogManager.class), Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class), @@ -119,7 +117,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh public static class Factory { private final Context mContext; - private final FeatureFlags mFeatureFlags; private final SystemUIDialogManager mSystemUIDialogManager; private final SysUiState mSysUiState; private final BroadcastDispatcher mBroadcastDispatcher; @@ -128,13 +125,11 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh @Inject public Factory( @Application Context context, - FeatureFlags featureFlags, SystemUIDialogManager systemUIDialogManager, SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; - mFeatureFlags = featureFlags; mSystemUIDialogManager = systemUIDialogManager; mSysUiState = sysUiState; mBroadcastDispatcher = broadcastDispatcher; @@ -177,7 +172,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK, - mFeatureFlags, mSystemUIDialogManager, mSysUiState, mBroadcastDispatcher, @@ -190,7 +184,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh Context context, int theme, boolean dismissOnDeviceLock, - FeatureFlags featureFlags, SystemUIDialogManager dialogManager, SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, @@ -199,7 +192,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh context, theme, dismissOnDeviceLock, - featureFlags, dialogManager, sysUiState, broadcastDispatcher, @@ -211,7 +203,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh Context context, int theme, boolean dismissOnDeviceLock, - FeatureFlags featureFlags, SystemUIDialogManager dialogManager, SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, @@ -221,7 +212,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh context, theme, dismissOnDeviceLock, - featureFlags, dialogManager, sysUiState, broadcastDispatcher, @@ -233,7 +223,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh Context context, int theme, boolean dismissOnDeviceLock, - FeatureFlags featureFlags, SystemUIDialogManager dialogManager, SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, @@ -241,7 +230,6 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh DialogDelegate<SystemUIDialog> delegate) { super(context, theme); mContext = context; - mFeatureFlags = featureFlags; mDelegate = delegate; applyFlags(this); @@ -269,7 +257,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh for (int i = 0; i < mOnCreateRunnables.size(); i++) { mOnCreateRunnables.get(i).run(); } - if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM)) { + if (predictiveBackAnimateDialogs()) { DialogKt.registerAnimationOnBackInvoked( /* dialog = */ this, /* targetView = */ getWindow().getDecorView() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt index d91ca92747f3..f3e8f62ddb5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt @@ -20,7 +20,6 @@ import android.content.Context import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.model.SysUiState import com.android.systemui.util.Assert import javax.inject.Inject @@ -30,7 +29,6 @@ class SystemUIDialogFactory @Inject constructor( @Application val applicationContext: Context, - private val featureFlags: FeatureFlagsClassic, private val dialogManager: SystemUIDialogManager, private val sysUiState: SysUiState, private val broadcastDispatcher: BroadcastDispatcher, @@ -57,7 +55,6 @@ constructor( context, theme, dismissOnDeviceLock, - featureFlags, dialogManager, sysUiState, broadcastDispatcher, 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 e1fd37f558ab..89a2fb78635b 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 @@ -29,6 +29,8 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.Airplane import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigCoreStartable import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher @@ -42,6 +44,8 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder @@ -76,8 +80,16 @@ abstract class StatusBarPipelineModule { abstract fun airplaneModeViewModel(impl: AirplaneModeViewModelImpl): AirplaneModeViewModel @Binds + abstract fun bindableIconsRepository(impl: BindableIconsRegistryImpl): BindableIconsRegistry + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository + @Binds + abstract fun deviceBasedSatelliteRepository( + impl: DeviceBasedSatelliteRepositoryImpl + ): DeviceBasedSatelliteRepository + @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor 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 new file mode 100644 index 000000000000..e3c3139f6906 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -0,0 +1,48 @@ +/* + * 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.icons.shared + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import javax.inject.Inject + +/** + * Bindable status bar icons represent icon descriptions which can be registered with + * StatusBarIconController and can also create their own bindings. A bound icon is responsible for + * its own updates via the [repeatWhenAttached] view lifecycle utility. Thus, + * StatusBarIconController can (and will) ignore any call to setIcon. + * + * In other words, these icons are bound once (at controller init) and they will control their + * visibility on their own (while their overall appearance remains at the discretion of + * StatusBarIconController, via the ModernStatusBarViewBinding interface). + */ +interface BindableIconsRegistry { + val bindableIcons: List<BindableIcon> +} + +@SysUISingleton +class BindableIconsRegistryImpl +@Inject +constructor( +/** Bindables go here */ +) : BindableIconsRegistry { + /** + * Adding the injected bindables to this list will get them registered with + * StatusBarIconController + */ + override val bindableIcons: List<BindableIcon> = listOf() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt new file mode 100644 index 000000000000..9d0d8380da45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt @@ -0,0 +1,55 @@ +/* + * 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.icons.shared.model + +/** + * A BindableIcon describes a status bar icon that can be housed in the [ModernStatusBarView] + * created by [initializer]. They can be registered statically for [BindableIconsRepositoryImpl]. + * + * Typical usage would be to create an (@SysUISingleton) adapter class that implements the + * interface. For example: + * ``` + * @SysuUISingleton + * class MyBindableIconAdapter + * @Inject constructor( + * // deps + * val viewModel: MyViewModel + * ) : BindableIcon { + * override val slot = "icon_slot_name" + * + * override val initializer = ModernStatusBarViewCreator() { + * SingleBindableStatusBarIconView.createView(context).also { iconView -> + * MyIconViewBinder.bind(iconView, viewModel) + * } + * } + * + * override fun shouldBind() = Flags.myFlag() + * } + * ``` + * + * By defining this adapter (and injecting it into the repository), we get our icon registered with + * the legacy StatusBarIconController while proxying all updates to the view binder that is created + * elsewhere. + * + * Note that the initializer block defines a closure that can pull in the viewModel dependency + * without us having to store it directly in the icon controller. + */ +interface BindableIcon { + val slot: String + val initializer: ModernStatusBarViewCreator + val shouldBindIcon: Boolean +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt new file mode 100644 index 000000000000..dbd5c1d1dede --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt @@ -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 com.android.systemui.statusbar.pipeline.icons.shared.model + +import android.content.Context +import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView + +/** + * Defined as an interface (as opposed to a typealias) to simplify calling from java. + * [ModernStatusBarViewCreator.createAndBind] should return a constructed and bound + * [ModernStatusBarView]. + */ +fun interface ModernStatusBarViewCreator { + fun createAndBind(context: Context): ModernStatusBarView +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index dad409316730..39135c70788d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -71,6 +71,12 @@ interface MobileIconsInteractor { /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow<List<SubscriptionModel>> + /** + * The current list of [MobileIconInteractor]s associated with the current list of + * [filteredSubscriptions] + */ + val icons: StateFlow<List<MobileIconInteractor>> + /** True if the active mobile data subscription has data enabled */ val activeDataConnectionHasDataEnabled: StateFlow<Boolean> @@ -259,6 +265,13 @@ constructor( } } + override val icons = + filteredSubscriptions + .mapLatest { subs -> + subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the * validated bit from the old active network (A) while data is changing to the new one (B). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt new file mode 100644 index 000000000000..ad8b8100f14d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt @@ -0,0 +1,37 @@ +/* + * 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.data + +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import kotlinx.coroutines.flow.Flow + +/** + * Device-based satellite refers to the capability of a device to connect directly to a satellite + * network. This is in contrast to carrier-based satellite connectivity, which is a property of a + * given mobile data subscription. + */ +interface DeviceBasedSatelliteRepository { + /** See [SatelliteConnectionState] for available states */ + val connectionState: Flow<SatelliteConnectionState> + + /** 0-4 level (similar to wifi and mobile) */ + // @IntRange(from = 0, to = 4) + val signalStrength: Flow<Int> + + /** Clients must observe this property, as device-based satellite is location-dependent */ + val isSatelliteAllowedForCurrentLocation: Flow<Boolean> +} 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 new file mode 100644 index 000000000000..8fc8b2f31366 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -0,0 +1,268 @@ +/* + * 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.data.prod + +import android.os.OutcomeReceiver +import android.telephony.satellite.NtnSignalStrengthCallback +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteStateCallback +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.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 +import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported +import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.util.kotlin.getOrNull +import com.android.systemui.util.time.SystemClock +import java.util.Optional +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +/** + * A SatelliteManager that has responded that it has satellite support. Use [SatelliteSupport] to + * get one + */ +private typealias SupportedSatelliteManager = SatelliteManager + +/** + * "Supported" here means supported by the device. The value of this should be stable during the + * process lifetime. + */ +private sealed interface SatelliteSupport { + /** Not yet fetched */ + data object Unknown : SatelliteSupport + + /** + * SatelliteManager says that this mode is supported. Note that satellite manager can never be + * null now + */ + data class Supported(val satelliteManager: SupportedSatelliteManager) : SatelliteSupport + + /** + * Either we were told that there is no support for this feature, or the manager is null, or + * some other exception occurred while querying for support. + */ + data object NotSupported : SatelliteSupport + + @OptIn(ExperimentalCoroutinesApi::class) + companion object { + /** Convenience function to switch to the supported flow */ + fun <T> Flow<SatelliteSupport>.whenSupported( + supported: (SatelliteManager) -> Flow<T>, + orElse: Flow<T>, + ): Flow<T> = flatMapLatest { + when (it) { + is Supported -> supported(it.satelliteManager) + else -> orElse + } + } + } +} + +/** + * Basically your everyday run-of-the-mill system service listener, with three notable exceptions. + * + * First, there is an availability bit that we are tracking via [SatelliteManager]. See + * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about + * this bit is that there is no callback that exists. Therefore we implement a simple polling + * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see + * [POLLING_INTERVAL_MS]) and see what the current state is. + * + * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See + * [SatelliteSupport] for details on how we track the state. What's worth noting here is that + * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental + * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method + * [SatelliteSupport.whenSupported]. By defining flows as simple functions based on a + * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told + * us that satellite is supported. Therefore, we don't expect exceptions to be thrown. + * + * Lastly, this class is designed to wait a full minute of process uptime before making any requests + * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that + * is still booting up or anything like that. We can tune or remove this behavior in the future if + * necessary. + */ +@SysUISingleton +class DeviceBasedSatelliteRepositoryImpl +@Inject +constructor( + satelliteManagerOpt: Optional<SatelliteManager>, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val systemClock: SystemClock, +) : DeviceBasedSatelliteRepository { + + private val satelliteManager: SatelliteManager? + + override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean> + + // Some calls into satellite manager will throw exceptions if it is not supported. + // This is never expected to change after boot, but may need to be retried in some cases + private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown) + + init { + satelliteManager = satelliteManagerOpt.getOrNull() + + isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) + + if (satelliteManager != null) { + // First, check that satellite is supported on this device + scope.launch { + ensureMinUptime(systemClock, MIN_UPTIME) + satelliteSupport.value = satelliteManager.checkSatelliteSupported() + + // We only need to check location availability if this mode is supported + if (satelliteSupport.value is Supported) { + isSatelliteAllowedForCurrentLocation.subscriptionCount + .map { it > 0 } + .distinctUntilChanged() + .collectLatest { hasSubscribers -> + if (hasSubscribers) { + /* + * As there is no listener available for checking satellite allowed, + * we must poll. Defaulting to polling at most once every hour while + * active. Subsequent OOS events will restart the job, so a flaky + * connection might cause more frequent checks. + */ + while (true) { + checkIsSatelliteAllowed() + delay(POLLING_INTERVAL_MS) + } + } + } + } + } + } else { + satelliteSupport.value = NotSupported + } + } + + override val connectionState = + satelliteSupport.whenSupported( + supported = ::connectionStateFlow, + orElse = flowOf(SatelliteConnectionState.Off) + ) + + // By using the SupportedSatelliteManager here, we expect registration never to fail + private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = + conflatedCallbackFlow { + val cb = SatelliteStateCallback { state -> + trySend(SatelliteConnectionState.fromModemState(state)) + } + + sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) + + awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) } + } + .flowOn(bgDispatcher) + + override val signalStrength = + satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0)) + + // By using the SupportedSatelliteManager here, we expect registration never to fail + private fun signalStrengthFlow(sm: SupportedSatelliteManager) = + conflatedCallbackFlow { + val cb = NtnSignalStrengthCallback { signalStrength -> + trySend(signalStrength.level) + } + + sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) + + awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) } + } + .flowOn(bgDispatcher) + + /** Fire off a request to check for satellite availability. Always runs on the bg context */ + private suspend fun checkIsSatelliteAllowed() = + withContext(bgDispatcher) { + satelliteManager?.requestIsSatelliteCommunicationAllowedForCurrentLocation( + bgDispatcher.asExecutor(), + object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { + override fun onError(e: SatelliteManager.SatelliteException) { + android.util.Log.e(TAG, "Found exception when checking for satellite: ", e) + isSatelliteAllowedForCurrentLocation.value = false + } + + override fun onResult(allowed: Boolean) { + isSatelliteAllowedForCurrentLocation.value = allowed + } + } + ) + } + + private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport = + suspendCancellableCoroutine { continuation -> + val cb = + object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { + override fun onResult(supported: Boolean) { + continuation.resume( + if (supported) { + Supported(satelliteManager = this@checkSatelliteSupported) + } else { + NotSupported + } + ) + } + + override fun onError(error: SatelliteManager.SatelliteException) { + // Assume that an error means it's not supported + continuation.resume(NotSupported) + } + } + + requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb) + } + + companion object { + // TTL for satellite polling is one hour + const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60 + + // Let the system boot up and stabilize before we check for system support + const val MIN_UPTIME: Long = 1000 * 60 + + private const val TAG = "DeviceBasedSatelliteRepo" + + /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */ + private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) { + val timeTilMinUptime = + uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis()) + if (timeTilMinUptime > 0) { + delay(timeTilMinUptime) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt new file mode 100644 index 000000000000..877957733d67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt @@ -0,0 +1,99 @@ +/* + * 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.domain.interactor + +import com.android.internal.telephony.flags.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +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.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class DeviceBasedSatelliteInteractor +@Inject +constructor( + val repo: DeviceBasedSatelliteRepository, + iconsInteractor: MobileIconsInteractor, + @Application scope: CoroutineScope, +) { + /** Must be observed by any UI showing Satellite iconography */ + val isSatelliteAllowed = + if (Flags.oemEnabledSatelliteFlag()) { + repo.isSatelliteAllowedForCurrentLocation + } else { + flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + /** See [SatelliteConnectionState] for relevant states */ + val connectionState = + if (Flags.oemEnabledSatelliteFlag()) { + repo.connectionState + } else { + + flowOf(SatelliteConnectionState.Off) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off) + + /** 0-4 description of the connection strength */ + val signalStrength = + if (Flags.oemEnabledSatelliteFlag()) { + repo.signalStrength + } else { + flowOf(0) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + + /** When all connections are considered OOS, satellite connectivity is potentially valid */ + val areAllConnectionsOutOfService = + if (Flags.oemEnabledSatelliteFlag()) { + iconsInteractor.icons.aggregateOver(selector = { intr -> intr.isInService }) { + isInServiceList -> + isInServiceList.all { !it } + } + } else { + flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) +} + +/** + * aggregateOver allows us to combine over the leaf-nodes of successive lists emitted from the + * top-level flow. Re-emits if the list changes, or any of the intermediate values change. + * + * Provides a way to connect the reactivity of the top-level flow with the reactivity of an + * arbitrarily-defined relationship ([selector]) from R to the flow that R exposes. + */ +@OptIn(ExperimentalCoroutinesApi::class) +private inline fun <R, reified S, T> Flow<List<R>>.aggregateOver( + crossinline selector: (R) -> Flow<S>, + crossinline transform: (Array<S>) -> T +): Flow<T> { + return map { list -> list.map { selector(it) } } + .flatMapLatest { newFlows -> combine(newFlows) { newVals -> transform(newVals) } } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt new file mode 100644 index 000000000000..bfe294119e64 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt @@ -0,0 +1,64 @@ +/* + * 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.shared.model + +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN + +enum class SatelliteConnectionState { + // State is unknown or undefined + Unknown, + // Radio is off + Off, + // Radio is on, but not yet connected + On, + // Radio is connected, aka satellite is available for use + Connected; + + companion object { + // TODO(b/316635648): validate these states. We don't need the level of granularity that + // SatelliteManager gives us. + fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) = + when (modemState) { + // Transferring data is connected + SATELLITE_MODEM_STATE_CONNECTED, + SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING, + SATELLITE_MODEM_STATE_DATAGRAM_RETRYING -> Connected + + // Modem is on but not connected + SATELLITE_MODEM_STATE_IDLE, + SATELLITE_MODEM_STATE_LISTENING, + SATELLITE_MODEM_STATE_NOT_CONNECTED -> On + + // Consider unavailable equivalent to Off + SATELLITE_MODEM_STATE_UNAVAILABLE, + SATELLITE_MODEM_STATE_OFF -> Off + + // Else, we don't know what's up + SATELLITE_MODEM_STATE_UNKNOWN -> Unknown + else -> Unknown + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 41ed76d7edb1..45078e32108d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -20,6 +20,7 @@ import static android.os.BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE; import static android.os.BatteryManager.CHARGING_POLICY_DEFAULT; import static android.os.BatteryManager.EXTRA_CHARGING_STATUS; import static android.os.BatteryManager.EXTRA_PRESENT; + import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS; import static com.android.systemui.util.DumpUtilsKt.asIndenting; @@ -61,6 +62,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import javax.annotation.concurrent.GuardedBy; @@ -448,50 +450,38 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC firePowerSaveChanged(); } - protected void fireBatteryLevelChanged() { - mLogger.logBatteryLevelChangedCallback(mLevel, mPluggedIn, mCharging); + protected final void dispatchSafeChange(Consumer<BatteryStateChangeCallback> action) { + ArrayList<BatteryStateChangeCallback> copy; synchronized (mChangeCallbacks) { - final int N = mChangeCallbacks.size(); - for (int i = 0; i < N; i++) { - mChangeCallbacks.get(i).onBatteryLevelChanged(mLevel, mPluggedIn, mCharging); - } + copy = new ArrayList<>(mChangeCallbacks); } + final int n = copy.size(); + for (int i = 0; i < n; i++) { + action.accept(copy.get(i)); + } + } + + protected void fireBatteryLevelChanged() { + mLogger.logBatteryLevelChangedCallback(mLevel, mPluggedIn, mCharging); + dispatchSafeChange( + (callback) -> callback.onBatteryLevelChanged(mLevel, mPluggedIn, mCharging)); } private void fireBatteryUnknownStateChanged() { - synchronized (mChangeCallbacks) { - final int n = mChangeCallbacks.size(); - for (int i = 0; i < n; i++) { - mChangeCallbacks.get(i).onBatteryUnknownStateChanged(mStateUnknown); - } - } + dispatchSafeChange((callback) -> callback.onBatteryUnknownStateChanged(mStateUnknown)); } private void firePowerSaveChanged() { - synchronized (mChangeCallbacks) { - final int N = mChangeCallbacks.size(); - for (int i = 0; i < N; i++) { - mChangeCallbacks.get(i).onPowerSaveChanged(mPowerSave); - } - } + dispatchSafeChange((callback) -> callback.onPowerSaveChanged(mPowerSave)); } private void fireIsBatteryDefenderChanged() { - synchronized (mChangeCallbacks) { - final int n = mChangeCallbacks.size(); - for (int i = 0; i < n; i++) { - mChangeCallbacks.get(i).onIsBatteryDefenderChanged(mIsBatteryDefender); - } - } + dispatchSafeChange((callback) -> callback.onIsBatteryDefenderChanged(mIsBatteryDefender)); } private void fireIsIncompatibleChargingChanged() { - synchronized (mChangeCallbacks) { - final int n = mChangeCallbacks.size(); - for (int i = 0; i < n; i++) { - mChangeCallbacks.get(i).onIsIncompatibleChargingChanged(mIsIncompatibleCharging); - } - } + dispatchSafeChange( + (callback) -> callback.onIsIncompatibleChargingChanged(mIsIncompatibleCharging)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 53b343c09329..fc2f6e958b32 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -436,6 +436,8 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa @Override public void onServiceDisconnected() {} + // IMPORTANT: This handler guarantees that any operations on the list of callbacks is + // sequential, so no concurrent exceptions private final class H extends Handler { private final ArrayList<BluetoothController.Callback> mCallbacks = new ArrayList<>(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java index b06ebe9b9358..149c8fa6f4ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java @@ -35,9 +35,9 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.annotations.GuardedBy; -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.util.Utils; import java.io.PrintWriter; @@ -291,11 +291,12 @@ public class CastControllerImpl implements CastController { @VisibleForTesting void fireOnCastDevicesChanged() { + final ArrayList<Callback> callbacks; synchronized (mCallbacks) { - for (Callback callback : mCallbacks) { - fireOnCastDevicesChanged(callback); - } - + callbacks = new ArrayList<>(mCallbacks); + } + for (Callback callback : callbacks) { + fireOnCastDevicesChanged(callback); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java index 8207012af6cc..6319781991e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java @@ -36,10 +36,12 @@ public class DataSaverControllerImpl implements DataSaverController { } private void handleRestrictBackgroundChanged(boolean isDataSaving) { + ArrayList<DataSaverController.Listener> copy; synchronized (mListeners) { - for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onDataSaverChanged(isDataSaving); - } + copy = new ArrayList<>(mListeners); + } + for (int i = 0; i < copy.size(); i++) { + copy.get(i).onDataSaverChanged(isDataSaving); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java index 5dcafb37f57e..b98eff8300a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java @@ -202,10 +202,12 @@ public class FlashlightControllerImpl implements FlashlightController { private void dispatchListeners(int message, boolean argument) { synchronized (mListeners) { - final int N = mListeners.size(); + final ArrayList<WeakReference<FlashlightController.FlashlightListener>> copy = + new ArrayList<>(mListeners); + final int n = copy.size(); boolean cleanup = false; - for (int i = 0; i < N; i++) { - FlashlightListener l = mListeners.get(i).get(); + for (int i = 0; i < n; i++) { + FlashlightListener l = copy.get(i).get(); if (l != null) { if (message == DISPATCH_ERROR) { l.onFlashlightError(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java index fffd839fcf11..87dfc9962675 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java @@ -119,7 +119,8 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr mHardwareToggleState.put(sensor, enabled); } - for (Callback callback : mCallbacks) { + Set<Callback> copy = new ArraySet<>(mCallbacks); + for (Callback callback : copy) { callback.onSensorBlockedChanged(sensor, isSensorBlocked(sensor)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java index e5f72ebdaab1..9eee5d00f708 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java @@ -356,6 +356,8 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio updateActiveLocationRequests(); } + // IMPORTANT: This handler guarantees that any operations on the list of callbacks is + // sequential, so no concurrent exceptions private final class H extends Handler { private static final int MSG_LOCATION_SETTINGS_CHANGED = 1; private static final int MSG_LOCATION_ACTIVE_CHANGED = 2; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java index 63b9ff9717d6..b7d8ee3943e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java @@ -124,9 +124,10 @@ public class NextAlarmControllerImpl extends BroadcastReceiver } private void fireNextAlarmChanged() { - int n = mChangeCallbacks.size(); + ArrayList<NextAlarmChangeCallback> copy = new ArrayList<>(mChangeCallbacks); + int n = copy.size(); for (int i = 0; i < n; i++) { - mChangeCallbacks.get(i).onNextAlarmChanged(mNextAlarm); + copy.get(i).onNextAlarmChanged(mNextAlarm); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java index f3d183ceb45f..0176abdf1579 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java @@ -100,10 +100,13 @@ public class SafetyController implements } private void handleSafetyCenterEnableChange() { + final ArrayList<SafetyController.Listener> copy; synchronized (mListeners) { - for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onSafetyCenterEnableChanged(mSafetyCenterEnabled); - } + copy = new ArrayList<>(mListeners); + } + final int n = copy.size(); + for (int i = 0; i < n; i++) { + copy.get(i).onSafetyCenterEnableChanged(mSafetyCenterEnabled); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 4a4d4e1f27b2..5d69f367d77e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -50,12 +50,12 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.GuardedBy; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; -import com.android.systemui.res.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import org.xmlpull.v1.XmlPullParserException; @@ -429,10 +429,12 @@ public class SecurityControllerImpl implements SecurityController { } private void fireCallbacks() { + final ArrayList<SecurityControllerCallback> copy; synchronized (mCallbacks) { - for (SecurityControllerCallback callback : mCallbacks) { - callback.onStateChanged(); - } + copy = new ArrayList<>(mCallbacks); + } + for (SecurityControllerCallback callback : copy) { + callback.onStateChanged(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index 66bf527f5047..df210b073e77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -48,12 +48,12 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; -import com.android.systemui.util.Utils; import com.android.systemui.util.settings.GlobalSettings; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; +import java.util.function.Consumer; import javax.inject.Inject; @@ -243,46 +243,43 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { } private void fireNextAlarmChanged() { - synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onNextAlarmChanged()); - } + fireSafeChange(Callback::onNextAlarmChanged); } private void fireEffectsSuppressorChanged() { - synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onEffectsSupressorChanged()); - } + fireSafeChange(Callback::onEffectsSupressorChanged); } private void fireZenChanged(int zen) { - synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onZenChanged(zen)); - } + fireSafeChange(c -> c.onZenChanged(zen)); } private void fireZenAvailableChanged(boolean available) { - synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onZenAvailableChanged(available)); - } + fireSafeChange(c -> c.onZenAvailableChanged(available)); } private void fireManualRuleChanged(ZenRule rule) { - synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onManualRuleChanged(rule)); - } + fireSafeChange(c -> c.onManualRuleChanged(rule)); } private void fireConsolidatedPolicyChanged(NotificationManager.Policy policy) { + fireSafeChange(c -> c.onConsolidatedPolicyChanged(policy)); + } + + private void fireSafeChange(Consumer<Callback> action) { + final ArrayList<Callback> copy; synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onConsolidatedPolicyChanged(policy)); + copy = new ArrayList<>(mCallbacks); + } + final int n = copy.size(); + for (int i = 0; i < n; i++) { + action.accept(copy.get(i)); } } @VisibleForTesting protected void fireConfigChanged(ZenModeConfig config) { - synchronized (mCallbacksLock) { - Utils.safeForeach(mCallbacks, c -> c.onConfigChanged(config)); - } + fireSafeChange(c -> c.onConfigChanged(config)); } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt b/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt index ac04d31041b6..4f7dce363a2b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt @@ -2,6 +2,7 @@ package com.android.systemui.util import java.lang.ref.SoftReference import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -48,3 +49,25 @@ fun <T> softReference(obj: T? = null): ReadWriteProperty<Any?, T?> { } } } + +/** + * Creates a nullable Kotlin idiomatic [AtomicReference]. + * + * Usage: + * ``` + * var atomicReferenceObj: Object? by nullableAtomicReference(null) + * atomicReferenceObj = Object() + * ``` + */ +fun <T> nullableAtomicReference(obj: T? = null): ReadWriteProperty<Any?, T?> { + return object : ReadWriteProperty<Any?, T?> { + val t = AtomicReference(obj) + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + return t.get() + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + t.set(value) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java index df5162af70c5..3d724e1caa5d 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java @@ -22,12 +22,17 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import android.util.IndentingPrintWriter; import android.util.Log; +import androidx.annotation.NonNull; + import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.annotations.WeaklyReferencedCallback; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; @@ -244,6 +249,21 @@ public class ObservableServiceConnection<T> implements ServiceConnection { }); } + void dump(@NonNull PrintWriter pw) { + IndentingPrintWriter ipw = DumpUtilsKt.asIndenting(pw); + ipw.println("ObservableServiceConnection state:"); + DumpUtilsKt.withIncreasedIndent(ipw, () -> { + ipw.println("mServiceIntent: " + mServiceIntent); + ipw.println("mLastDisconnectReason: " + mLastDisconnectReason.orElse(-1)); + ipw.println("Callbacks:"); + DumpUtilsKt.withIncreasedIndent(ipw, () -> { + for (WeakReference<Callback<T>> cbRef : mCallbacks) { + ipw.println(cbRef.get()); + } + }); + }); + } + private void applyToCallbacksLocked(Consumer<Callback<T>> applicator) { final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator(); diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java index 6e19bed49626..9b72eb710588 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java @@ -17,6 +17,7 @@ package com.android.systemui.util.service; import static com.android.systemui.util.service.dagger.ObservableServiceModule.BASE_RECONNECT_DELAY_MS; +import static com.android.systemui.util.service.dagger.ObservableServiceModule.DUMPSYS_NAME; import static com.android.systemui.util.service.dagger.ObservableServiceModule.MAX_RECONNECT_ATTEMPTS; import static com.android.systemui.util.service.dagger.ObservableServiceModule.MIN_CONNECTION_DURATION_MS; import static com.android.systemui.util.service.dagger.ObservableServiceModule.OBSERVER; @@ -24,9 +25,15 @@ import static com.android.systemui.util.service.dagger.ObservableServiceModule.S import android.util.Log; +import androidx.annotation.NonNull; + +import com.android.systemui.Dumpable; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.time.SystemClock; +import java.io.PrintWriter; + import javax.inject.Inject; import javax.inject.Named; @@ -35,7 +42,7 @@ import javax.inject.Named; * {@link ObservableServiceConnection}. * @param <T> The transformed connection type handled by the service. */ -public class PersistentConnectionManager<T> { +public class PersistentConnectionManager<T> implements Dumpable { private static final String TAG = "PersistentConnManager"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -45,6 +52,8 @@ public class PersistentConnectionManager<T> { private final int mMaxReconnectAttempts; private final int mMinConnectionDuration; private final Observer mObserver; + private final DumpManager mDumpManager; + private final String mDumpsysName; private int mReconnectAttempts = 0; private Runnable mCurrentReconnectCancelable; @@ -89,6 +98,8 @@ public class PersistentConnectionManager<T> { public PersistentConnectionManager( SystemClock clock, DelayableExecutor mainExecutor, + DumpManager dumpManager, + @Named(DUMPSYS_NAME) String dumpsysName, @Named(SERVICE_CONNECTION) ObservableServiceConnection<T> serviceConnection, @Named(MAX_RECONNECT_ATTEMPTS) int maxReconnectAttempts, @Named(BASE_RECONNECT_DELAY_MS) int baseReconnectDelayMs, @@ -98,6 +109,8 @@ public class PersistentConnectionManager<T> { mMainExecutor = mainExecutor; mConnection = serviceConnection; mObserver = observer; + mDumpManager = dumpManager; + mDumpsysName = TAG + "#" + dumpsysName; mMaxReconnectAttempts = maxReconnectAttempts; mBaseReconnectDelayMs = baseReconnectDelayMs; @@ -108,6 +121,7 @@ public class PersistentConnectionManager<T> { * Begins the {@link PersistentConnectionManager} by connecting to the associated service. */ public void start() { + mDumpManager.registerCriticalDumpable(mDumpsysName, this); mConnection.addCallback(mConnectionCallback); mObserver.addCallback(mObserverCallback); initiateConnectionAttempt(); @@ -120,6 +134,32 @@ public class PersistentConnectionManager<T> { mConnection.removeCallback(mConnectionCallback); mObserver.removeCallback(mObserverCallback); mConnection.unbind(); + mDumpManager.unregisterDumpable(mDumpsysName); + } + + /** + * Add a callback to the {@link ObservableServiceConnection}. + * @param callback The callback to add. + */ + public void addConnectionCallback(ObservableServiceConnection.Callback<T> callback) { + mConnection.addCallback(callback); + } + + /** + * Remove a callback from the {@link ObservableServiceConnection}. + * @param callback The callback to remove. + */ + public void removeConnectionCallback(ObservableServiceConnection.Callback<T> callback) { + mConnection.removeCallback(callback); + } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("mMaxReconnectAttempts: " + mMaxReconnectAttempts); + pw.println("mBaseReconnectDelayMs: " + mBaseReconnectDelayMs); + pw.println("mMinConnectionDuration: " + mMinConnectionDuration); + pw.println("mReconnectAttempts: " + mReconnectAttempts); + mConnection.dump(pw); } private void initiateConnectionAttempt() { diff --git a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java index bcf34f833d32..c52c524d1fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java @@ -19,14 +19,14 @@ package com.android.systemui.util.service.dagger; import android.content.res.Resources; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Main; - -import javax.inject.Named; +import com.android.systemui.res.R; import dagger.Module; import dagger.Provides; +import javax.inject.Named; + /** * Module containing components and parameters for * {@link com.android.systemui.util.service.ObservableServiceConnection} @@ -41,6 +41,7 @@ public class ObservableServiceModule { public static final String MIN_CONNECTION_DURATION_MS = "min_connection_duration_ms"; public static final String SERVICE_CONNECTION = "service_connection"; public static final String OBSERVER = "observer"; + public static final String DUMPSYS_NAME = "dumpsys_name"; @Provides @Named(MAX_RECONNECT_ATTEMPTS) diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt index 342494d8997b..46936d6223a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository import com.android.systemui.decor.FaceScanningProviderFactory import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.log.logcatLogBuffer @@ -53,6 +54,8 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + private val facePropertyRepository = FakeFacePropertyRepository() + private val displayId = 2 @Before @@ -86,9 +89,10 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { keyguardUpdateMonitor, mock(Executor::class.java), ScreenDecorationsLogger(logcatLogBuffer("FaceScanningProviderFactoryTest")), + facePropertyRepository, ) - whenever(authController.faceSensorLocation).thenReturn(Point(10, 10)) + facePropertyRepository.setSensorLocation(Point(10, 10)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index c094df5dd569..c07148b32cf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -54,6 +54,7 @@ import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Path; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; @@ -80,6 +81,7 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository; import com.android.systemui.decor.CornerDecorProvider; import com.android.systemui.decor.CutoutDecorProviderFactory; import com.android.systemui.decor.CutoutDecorProviderImpl; @@ -101,6 +103,7 @@ import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; @@ -108,8 +111,6 @@ import com.android.systemui.util.time.FakeSystemClock; 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.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; @@ -169,8 +170,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener; @Mock private CutoutDecorProviderFactory mCutoutFactory; - @Captor - private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback; + @Mock + private JavaAdapter mJavaAdapter; + + private FakeFacePropertyRepository mFakeFacePropertyRepository = + new FakeFacePropertyRepository(); private List<DecorProvider> mMockCutoutList; @Before @@ -227,20 +231,23 @@ public class ScreenDecorationsTest extends SysuiTestCase { doAnswer(it -> !(mMockCutoutList.isEmpty())).when(mCutoutFactory).getHasProviders(); doReturn(mMockCutoutList).when(mCutoutFactory).getProviders(); + mFakeFacePropertyRepository.setSensorLocation(new Point(10, 10)); + mFaceScanningDecorProvider = spy(new FaceScanningOverlayProviderImpl( BOUNDS_POSITION_TOP, mAuthController, mStatusBarStateController, mKeyguardUpdateMonitor, mExecutor, - new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")))); + new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), + mFakeFacePropertyRepository)); mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), - mAuthController) { + mFakeFacePropertyRepository, mJavaAdapter) { @Override public void start() { super.start(); @@ -1235,9 +1242,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, - new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController); + new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), + mFakeFacePropertyRepository, mJavaAdapter); screenDecorations.start(); - verify(mAuthController).addCallback(mAuthControllerCallback.capture()); when(mContext.getDisplay()).thenReturn(mDisplay); when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() { @Override @@ -1252,9 +1259,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { }); mExecutor.runAllReady(); clearInvocations(mFaceScanningDecorProvider); - - AuthController.Callback callback = mAuthControllerCallback.getValue(); - callback.onFaceSensorLocationChanged(); + final Point location = new Point(); + mFakeFacePropertyRepository.setSensorLocation(location); + screenDecorations.onFaceSensorLocationChanged(location); mExecutor.runAllReady(); verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(), 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 837a13073eea..2afb3a15b4be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -43,7 +43,6 @@ import android.view.WindowManagerGlobal; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.animation.AccelerateInterpolator; -import androidx.test.filters.FlakyTest; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -68,7 +67,6 @@ import java.util.concurrent.atomic.AtomicReference; @LargeTest @RunWith(AndroidTestingRunner.class) -@FlakyTest(bugId = 308501761) public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt index c52571188256..9b6c8cdf7f9d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt @@ -25,14 +25,12 @@ import android.view.ViewGroup import android.widget.Button import android.widget.SeekBar import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.model.SysUiState +import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK @@ -78,7 +76,6 @@ class FontScalingDialogDelegateTest : SysuiTestCase() { @Mock private lateinit var dialogManager: SystemUIDialogManager @Mock private lateinit var dialogFactory: SystemUIDialog.Factory @Mock private lateinit var userTracker: UserTracker - private val featureFlags = FakeFeatureFlags() @Mock private lateinit var sysuiState: SysUiState @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator @@ -88,7 +85,6 @@ class FontScalingDialogDelegateTest : SysuiTestCase() { testableLooper = TestableLooper.get(this) val mainHandler = Handler(testableLooper.looper) systemSettings = FakeSettings() - featureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true) // Guarantee that the systemSettings always starts with the default font scale. systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId) secureSettings = FakeSettings() @@ -96,29 +92,32 @@ class FontScalingDialogDelegateTest : SysuiTestCase() { backgroundDelayableExecutor = FakeExecutor(systemClock) whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState) - fontScalingDialogDelegate = spy(FontScalingDialogDelegate( + fontScalingDialogDelegate = + spy( + FontScalingDialogDelegate( + mContext, + dialogFactory, + LayoutInflater.from(mContext), + systemSettings, + secureSettings, + systemClock, + userTracker, + mainHandler, + backgroundDelayableExecutor + ) + ) + + dialog = + SystemUIDialog( mContext, - dialogFactory, - LayoutInflater.from(mContext), - systemSettings, - secureSettings, - systemClock, - userTracker, - mainHandler, - backgroundDelayableExecutor - )) - - dialog = SystemUIDialog( - mContext, - 0, - DEFAULT_DISMISS_ON_DEVICE_LOCK, - featureFlags, - dialogManager, - sysuiState, - fakeBroadcastDispatcher, - dialogLaunchAnimator, - fontScalingDialogDelegate - ) + 0, + DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogLaunchAnimator, + fontScalingDialogDelegate + ) whenever(dialogFactory.create(any(), any())).thenReturn(dialog) } @@ -299,11 +298,7 @@ class FontScalingDialogDelegateTest : SysuiTestCase() { // Default seekbar progress for font size is 1, simulate dragging to 0 without // releasing the finger changeListener.onStartTrackingTouch(seekBar) - changeListener.onProgressChanged( - seekBar, - /* progress= */ 0, - /* fromUser= */ false - ) + changeListener.onProgressChanged(seekBar, /* progress= */ 0, /* fromUser= */ false) backgroundDelayableExecutor.advanceClockToNext() backgroundDelayableExecutor.runAllReady() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index c143bc0aa06c..a47e28801709 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -29,6 +29,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.logging.KeyguardLogger import com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository import com.android.systemui.log.logcatLogBuffer import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.WakefulnessLifecycle @@ -93,6 +94,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal + private val facePropertyRepository = FakeFacePropertyRepository() private val displayMetrics = DisplayMetrics() @Captor @@ -126,6 +128,7 @@ class AuthRippleControllerTest : SysuiTestCase() { KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), biometricUnlockController, lightRevealScrim, + facePropertyRepository, rippleView, ) controller.init() @@ -202,7 +205,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Test fun testNullFaceSensorLocationDoesNothing() { - `when`(authController.faceSensorLocation).thenReturn(null) + facePropertyRepository.setSensorLocation(null) controller.onViewAttached() val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) @@ -270,7 +273,7 @@ class AuthRippleControllerTest : SysuiTestCase() { fun testAnimatorRunWhenWakeAndUnlock_faceUdfpsFingerDown() { mSetFlagsRule.disableFlags(FLAG_LIGHT_REVEAL_MIGRATION) val faceLocation = Point(5, 5) - `when`(authController.faceSensorLocation).thenReturn(faceLocation) + facePropertyRepository.setSensorLocation(faceLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java index 2aeba9a09b84..3dcb3f89c730 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.content.res.Resources; import android.graphics.Rect; +import android.hardware.fingerprint.FingerprintSensorProperties; import android.view.Surface; import androidx.test.filters.SmallTest; @@ -63,28 +64,32 @@ public class UdfpsUtilsTest extends SysuiTestCase { assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, 0/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[0]); // touch at 90 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, -1/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[1]); // touch at 180 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, -1 /* touchX */, 0/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[2]); // touch at 270 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, 1/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[3]); } @@ -97,28 +102,32 @@ public class UdfpsUtilsTest extends SysuiTestCase { assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, 0 /* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[1]); // touch at 90 degrees -> 180 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, -1 /* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[2]); // touch at 180 degrees -> 270 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, -1 /* touchX */, 0 /* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[3]); // touch at 270 degrees -> 0 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, 1/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[0]); } @@ -131,28 +140,32 @@ public class UdfpsUtilsTest extends SysuiTestCase { assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, 0/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[3]); // touch at 90 degrees -> 0 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, -1/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[0]); // touch at 180 degrees -> 90 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, -1 /* touchX */, 0/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[1]); // touch at 270 degrees -> 180 degrees assertThat( mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, 0 /* touchX */, 1/* touchY */, - new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) ) ).isEqualTo(mTouchHints[2]); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt index 834179bf289d..a84778a49643 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository import android.hardware.devicestate.DeviceStateManager import android.hardware.display.DisplayManager import android.os.Handler +import android.util.Size import android.view.Display import android.view.DisplayInfo import android.view.Surface @@ -147,6 +148,40 @@ class DisplayStateRepositoryTest : SysuiTestCase() { displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_180) assertThat(currentRotation).isEqualTo(DisplayRotation.ROTATION_180) } + + @Test + fun updatesCurrentSize_whenDisplayStateChanges() = + testScope.runTest { + val currentSize by collectLastValue(underTest.currentDisplaySize) + runCurrent() + + verify(displayManager) + .registerDisplayListener( + displayListenerCaptor.capture(), + same(handler), + eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) + ) + + whenever(display.getDisplayInfo(any())).then { + val info = it.getArgument<DisplayInfo>(0) + info.rotation = Surface.ROTATION_0 + info.logicalWidth = 100 + info.logicalHeight = 200 + return@then true + } + displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_0) + assertThat(currentSize).isEqualTo(Size(100, 200)) + + whenever(display.getDisplayInfo(any())).then { + val info = it.getArgument<DisplayInfo>(0) + info.rotation = Surface.ROTATION_90 + info.logicalWidth = 100 + info.logicalHeight = 200 + return@then true + } + displayListenerCaptor.value.onDisplayChanged(Surface.ROTATION_180) + assertThat(currentSize).isEqualTo(Size(200, 100)) + } } private fun DeviceStateManager.captureCallback() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt index c14ad6a46616..9f24d5dfea79 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt @@ -17,10 +17,12 @@ package com.android.systemui.biometrics.data.repository +import android.graphics.Point import android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_NONE import android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_PERMANENT import android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED import android.hardware.biometrics.SensorProperties +import android.hardware.camera2.CameraManager import android.hardware.face.FaceManager import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.face.IFaceAuthenticatorsRegisteredCallback @@ -28,9 +30,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.shared.model.LockoutMode import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.res.R import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher @@ -45,6 +50,7 @@ import org.junit.runners.JUnit4 import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.any import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -53,23 +59,56 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(JUnit4::class) class FacePropertyRepositoryImplTest : SysuiTestCase() { + companion object { + private const val LOGICAL_CAMERA_ID_OUTER_FRONT = "0" + private const val LOGICAL_CAMERA_ID_INNER_FRONT = "1" + private const val PHYSICAL_CAMERA_ID_OUTER_FRONT = "5" + private const val PHYSICAL_CAMERA_ID_INNER_FRONT = "6" + private val OUTER_FRONT_SENSOR_LOCATION = intArrayOf(100, 10, 20) + private val INNER_FRONT_SENSOR_LOCATION = intArrayOf(200, 20, 30) + } + @JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private lateinit var underTest: FacePropertyRepository private lateinit var dispatcher: TestDispatcher private lateinit var testScope: TestScope + private val displayStateRepository = FakeDisplayStateRepository() + private val configurationRepository = FakeConfigurationRepository() + @Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> @Mock private lateinit var faceManager: FaceManager + @Captor private lateinit var cameraCallback: ArgumentCaptor<CameraManager.AvailabilityCallback> + @Mock private lateinit var cameraManager: CameraManager @Before fun setup() { + overrideResource(R.string.config_protectedCameraId, LOGICAL_CAMERA_ID_OUTER_FRONT) + overrideResource(R.string.config_protectedPhysicalCameraId, PHYSICAL_CAMERA_ID_OUTER_FRONT) + overrideResource(R.string.config_protectedInnerCameraId, LOGICAL_CAMERA_ID_INNER_FRONT) + overrideResource( + R.string.config_protectedInnerPhysicalCameraId, + PHYSICAL_CAMERA_ID_INNER_FRONT + ) + overrideResource(R.array.config_face_auth_props, OUTER_FRONT_SENSOR_LOCATION) + overrideResource(R.array.config_inner_face_auth_props, INNER_FRONT_SENSOR_LOCATION) + dispatcher = StandardTestDispatcher() testScope = TestScope(dispatcher) underTest = createRepository(faceManager) } private fun createRepository(manager: FaceManager? = faceManager) = - FacePropertyRepositoryImpl(testScope.backgroundScope, dispatcher, manager) + FacePropertyRepositoryImpl( + context, + context.mainExecutor, + testScope.backgroundScope, + dispatcher, + manager, + cameraManager, + displayStateRepository, + configurationRepository, + ) @Test fun whenFaceManagerIsNotPresentIsNull() = @@ -129,6 +168,75 @@ class FacePropertyRepositoryImplTest : SysuiTestCase() { assertThat(underTest.getLockoutMode(userId)).isEqualTo(LockoutMode.NONE) } + @Test + fun providesTheSensorLocationOfOuterCameraFromOnPhysicalCameraAvailable() { + testScope.runTest { + runCurrent() + collectLastValue(underTest.sensorLocation) + + verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture()) + callback.value.onAllAuthenticatorsRegistered( + listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG)) + ) + runCurrent() + verify(cameraManager) + .registerAvailabilityCallback(any(Executor::class.java), cameraCallback.capture()) + + cameraCallback.value.onPhysicalCameraAvailable("1", PHYSICAL_CAMERA_ID_OUTER_FRONT) + runCurrent() + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation) + .isEqualTo(Point(OUTER_FRONT_SENSOR_LOCATION[0], OUTER_FRONT_SENSOR_LOCATION[1])) + } + } + + @Test + fun providesTheSensorLocationOfInnerCameraFromOnPhysicalCameraAvailable() { + testScope.runTest { + runCurrent() + collectLastValue(underTest.sensorLocation) + + verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture()) + callback.value.onAllAuthenticatorsRegistered( + listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG)) + ) + runCurrent() + verify(cameraManager) + .registerAvailabilityCallback(any(Executor::class.java), cameraCallback.capture()) + + cameraCallback.value.onPhysicalCameraAvailable("1", PHYSICAL_CAMERA_ID_INNER_FRONT) + runCurrent() + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation) + .isEqualTo(Point(INNER_FRONT_SENSOR_LOCATION[0], INNER_FRONT_SENSOR_LOCATION[1])) + } + } + + @Test + fun providesTheSensorLocationOfCameraFromOnPhysicalCameraUnavailable() { + testScope.runTest { + runCurrent() + collectLastValue(underTest.sensorLocation) + + verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture()) + callback.value.onAllAuthenticatorsRegistered( + listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG)) + ) + runCurrent() + verify(cameraManager) + .registerAvailabilityCallback(any(Executor::class.java), cameraCallback.capture()) + + cameraCallback.value.onPhysicalCameraUnavailable("1", PHYSICAL_CAMERA_ID_INNER_FRONT) + runCurrent() + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation) + .isEqualTo(Point(OUTER_FRONT_SENSOR_LOCATION[0], OUTER_FRONT_SENSOR_LOCATION[1])) + } + } + private fun createSensorProperties(id: Int, strength: Int) = FaceSensorPropertiesInternal(id, strength, 0, emptyList(), 1, false, false, false) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java index 3ff43c6a3787..7d5aec6b7d4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java @@ -17,7 +17,9 @@ package com.android.systemui.bluetooth; import static com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; @@ -40,8 +42,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.model.SysUiState; import com.android.systemui.res.R; @@ -72,7 +72,6 @@ public class BroadcastDialogDelegateTest extends SysuiTestCase { LocalBluetoothLeBroadcast.class); private final BroadcastSender mBroadcastSender = mock(BroadcastSender.class); private BroadcastDialogDelegate mBroadcastDialogDelegate; - private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock SystemUIDialog.Factory mSystemUIDialogFactory; @Mock SystemUIDialogManager mDialogManager; @Mock SysUiState mSysUiState; @@ -91,7 +90,6 @@ public class BroadcastDialogDelegateTest extends SysuiTestCase { when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(null); - mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true); when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog); @@ -110,7 +108,6 @@ public class BroadcastDialogDelegateTest extends SysuiTestCase { mContext, 0, DEFAULT_DISMISS_ON_DEVICE_LOCK, - mFeatureFlags, mDialogManager, mSysUiState, getFakeBroadcastDispatcher(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 0a5b124cdb7b..fb101dda6aaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -78,7 +78,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mMediaData = new MediaData( USER_ID, true, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, - MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, + MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, 0L, InstanceId.fakeInstanceId(-1), -1, false, null); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java index 35bf7753358e..dc211303e52c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java @@ -83,7 +83,6 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { public void setup() { MockitoAnnotations.initMocks(this); - mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true); when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); when(mSystemUIDialog.getContext()).thenReturn(mContext); diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index 7ce51aea90e2..86ab01ca9e2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -105,7 +105,6 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { spy( SystemUIDialog.Factory( context, - flags, systemUIDialogManager, sysuiState, broadcastDispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index cb90cc53f0f0..0ba99f2d24fb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -44,7 +44,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.mediaprojection.SessionCreationSource; @@ -113,7 +112,6 @@ public class RecordingControllerTest extends SysuiTestCase { mDialogFactory = new TestSystemUIDialogFactory( mContext, - mFeatureFlags, Dependency.get(SystemUIDialogManager.class), Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class), @@ -313,14 +311,12 @@ public class RecordingControllerTest extends SysuiTestCase { TestSystemUIDialogFactory( Context context, - FeatureFlags featureFlags, SystemUIDialogManager systemUIDialogManager, SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) { super( context, - featureFlags, systemUIDialogManager, sysUiState, broadcastDispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt index 8f696e7f11ad..23995364f9e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt @@ -74,7 +74,6 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { val systemUIDialogFactory = SystemUIDialog.Factory( context, - Dependency.get(FeatureFlags::class.java), Dependency.get(SystemUIDialogManager::class.java), Dependency.get(SysUiState::class.java), Dependency.get(BroadcastDispatcher::class.java), 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 313276727caf..a20658197a8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -152,7 +152,9 @@ import com.android.systemui.statusbar.notification.ConversationNotificationManag import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger; +import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository; import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -586,6 +588,10 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); + NotificationsKeyguardViewStateRepository notifsKeyguardViewStateRepository = + new NotificationsKeyguardViewStateRepository(); + NotificationsKeyguardInteractor notifsKeyguardInteractor = + new NotificationsKeyguardInteractor(notifsKeyguardViewStateRepository); NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( mDumpManager, @@ -596,7 +602,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardBypassController, mDozeParameters, mScreenOffAnimationController, - new NotificationWakeUpCoordinatorLogger(logcatLogBuffer())); + new NotificationWakeUpCoordinatorLogger(logcatLogBuffer()), + notifsKeyguardInteractor); mConfigurationController = new ConfigurationControllerImpl(mContext); PulseExpansionHandler expansionHandler = new PulseExpansionHandler( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 438b33d9afbc..039fef9c1df5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -22,12 +22,14 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.dump.DumpManager +import com.android.systemui.kosmos.Kosmos import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeViewController.Companion.WAKEUP_ANIMATION_DELAY_MS import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_WAKEUP +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController @@ -54,6 +56,8 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() + private val kosmos = Kosmos() + private val dumpManager: DumpManager = mock() private val headsUpManager: HeadsUpManager = mock() private val statusBarStateController: StatusBarStateController = mock() @@ -100,6 +104,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { dozeParameters, screenOffAnimationController, logger, + kosmos.notificationsKeyguardInteractor, ) statusBarStateCallback = withArgCaptor { verify(statusBarStateController).addCallback(capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt deleted file mode 100644 index 170f651aed91..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt +++ /dev/null @@ -1,88 +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.notification.data.repository - -import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule -import com.android.systemui.SysuiTestCase -import com.android.systemui.collectLastValue -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.runCurrent -import com.android.systemui.runTest -import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.mockito.withArgCaptor -import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component -import org.junit.Test -import org.mockito.Mockito.verify - -@SmallTest -class NotificationsKeyguardViewStateRepositoryTest : SysuiTestCase() { - - @SysUISingleton - @Component(modules = [SysUITestModule::class]) - interface TestComponent : SysUITestComponent<NotificationsKeyguardViewStateRepositoryImpl> { - - val mockWakeUpCoordinator: NotificationWakeUpCoordinator - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - ): TestComponent - } - } - - private val testComponent: TestComponent = - DaggerNotificationsKeyguardViewStateRepositoryTest_TestComponent.factory() - .create(test = this) - - @Test - fun areNotifsFullyHidden_reflectsWakeUpCoordinator() = - testComponent.runTest { - whenever(mockWakeUpCoordinator.notificationsFullyHidden).thenReturn(false) - val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden) - runCurrent() - - assertThat(notifsFullyHidden).isFalse() - - withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) } - .onFullyHiddenChanged(true) - runCurrent() - - assertThat(notifsFullyHidden).isTrue() - } - - @Test - fun isPulseExpanding_reflectsWakeUpCoordinator() = - testComponent.runTest { - whenever(mockWakeUpCoordinator.isPulseExpanding()).thenReturn(false) - val isPulseExpanding by collectLastValue(underTest.isPulseExpanding) - runCurrent() - - assertThat(isPulseExpanding).isFalse() - - withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) } - .onPulseExpandingChanged(true) - runCurrent() - - assertThat(isPulseExpanding).isTrue() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt index bb3113a72e92..3593f5b4963e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt @@ -21,7 +21,6 @@ import com.android.systemui.collectLastValue import com.android.systemui.dagger.SysUISingleton import com.android.systemui.runCurrent import com.android.systemui.runTest -import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -33,9 +32,6 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { @SysUISingleton @Component(modules = [SysUITestModule::class]) interface TestComponent : SysUITestComponent<NotificationsKeyguardInteractor> { - - val repository: FakeNotificationsKeyguardViewStateRepository - @Component.Factory interface Factory { fun create(@BindsInstance test: SysuiTestCase): TestComponent @@ -48,13 +44,13 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { @Test fun areNotifsFullyHidden_reflectsRepository() = testComponent.runTest { - repository.setNotificationsFullyHidden(false) + underTest.setNotificationsFullyHidden(false) val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden) runCurrent() assertThat(notifsFullyHidden).isFalse() - repository.setNotificationsFullyHidden(true) + underTest.setNotificationsFullyHidden(true) runCurrent() assertThat(notifsFullyHidden).isTrue() @@ -63,13 +59,13 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { @Test fun isPulseExpanding_reflectsRepository() = testComponent.runTest { - repository.setPulseExpanding(false) + underTest.setPulseExpanding(false) val isPulseExpanding by collectLastValue(underTest.isPulseExpanding) runCurrent() assertThat(isPulseExpanding).isFalse() - repository.setPulseExpanding(true) + underTest.setPulseExpanding(true) runCurrent() assertThat(isPulseExpanding).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index 47feccf4bdcf..7faf5628b40a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -29,8 +29,8 @@ import com.android.systemui.statusbar.data.repository.NotificationListenerSettin import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore -import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.notification.shared.byIsAmbient import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply import com.android.systemui.statusbar.notification.shared.byIsPulsing @@ -61,7 +61,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { interface TestComponent : SysUITestComponent<NotificationIconsInteractor> { val activeNotificationListRepository: ActiveNotificationListRepository - val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository + val notificationsKeyguardInteractor: NotificationsKeyguardInteractor @Component.Factory interface Factory { @@ -136,7 +136,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { fun filteredEntrySet_noPulsing_notifsNotFullyHidden() = testComponent.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false)) - keyguardViewStateRepository.setNotificationsFullyHidden(false) + notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true) } @@ -144,7 +144,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { fun filteredEntrySet_noPulsing_notifsFullyHidden() = testComponent.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false)) - keyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } } @@ -161,7 +161,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { val activeNotificationListRepository: ActiveNotificationListRepository val deviceEntryRepository: FakeDeviceEntryRepository - val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository + val notificationsKeyguardInteractor: NotificationsKeyguardInteractor @Component.Factory interface Factory { @@ -222,7 +222,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(false) - keyguardViewStateRepository.setNotificationsFullyHidden(false) + notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } @@ -231,7 +231,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(false) - keyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } @@ -240,7 +240,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(true) - keyguardViewStateRepository.setNotificationsFullyHidden(false) + notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true) } @@ -249,7 +249,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(true) - keyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } } @@ -266,7 +266,7 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { val activeNotificationListRepository: ActiveNotificationListRepository val headsUpIconsInteractor: HeadsUpNotificationIconInteractor - val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository + val notificationsKeyguardInteractor: NotificationsKeyguardInteractor val notificationListenerSettingsRepository: NotificationListenerSettingsRepository @Component.Factory diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt index 7eba3b463336..c44c178db38a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt @@ -22,11 +22,15 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import junit.framework.Assert import org.junit.Before import org.junit.Test import org.mockito.Mock +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -72,6 +76,34 @@ class ManagedProfileControllerImplTest : SysuiTestCase() { Assert.assertEquals(true, controller.hasActiveProfile()) } + @Test + fun callbackRemovedWhileDispatching_doesntCrash() { + var remove = false + val callback = + object : ManagedProfileController.Callback { + override fun onManagedProfileChanged() { + if (remove) { + controller.removeCallback(this) + } + } + + override fun onManagedProfileRemoved() { + if (remove) { + controller.removeCallback(this) + } + } + } + controller.addCallback(callback) + controller.addCallback(TestCallback) + + remove = true + setupWorkingProfile(1) + + val captor = argumentCaptor<UserTracker.Callback>() + verify(userTracker).addCallback(capture(captor), any()) + captor.value.onProfilesChanged(userManager.getEnabledProfiles(1)) + } + private fun setupWorkingProfile(userId: Int) { `when`(userManager.getEnabledProfiles(userId)) .thenReturn( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt index 6f04f369d52f..f6a8243c7a46 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt @@ -23,6 +23,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat @@ -49,14 +52,15 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { iconList = StatusBarIconList(arrayOf()) underTest = StatusBarIconControllerImpl( - context, - commandQueue, - mock(), - mock(), - mock(), - mock(), - iconList, - mock(), + /* context = */ context, + /* commandQueue = */ commandQueue, + /* demoModeController = */ mock(), + /* configurationController = */ mock(), + /* tunerService = */ mock(), + /* dumpManager = */ mock(), + /* statusBarIconList = */ iconList, + /* statusBarPipelineFlags = */ mock(), + /* modernIconsRegistry = */ mock(), ) underTest.addIconGroup(iconGroup) val commandQueueCallbacksCaptor = kotlinArgumentCaptor<CommandQueue.Callbacks>() @@ -366,6 +370,31 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX") } + @Test + fun bindableIcons_addedOnInit() { + val fakeIcon = FakeBindableIcon("test_slot") + + iconList = StatusBarIconList(arrayOf()) + + // WHEN there are registered icons + underTest = + StatusBarIconControllerImpl( + /* context = */ context, + /* commandQueue = */ commandQueue, + /* demoModeController = */ mock(), + /* configurationController = */ mock(), + /* tunerService = */ mock(), + /* dumpManager = */ mock(), + /* statusBarIconList = */ iconList, + /* statusBarPipelineFlags = */ mock(), + /* modernIconsRegistry = */ FakeBindableIconsRegistry(listOf(fakeIcon)), + ) + + // THEN they are properly added to the list on init + assertThat(iconList.getIconHolder("test_slot", 0)) + .isInstanceOf(StatusBarIconHolder.BindableIconHolder::class.java) + } + private fun createExternalIcon(): StatusBarIcon { return StatusBarIcon( "external.package", @@ -377,3 +406,20 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { ) } } + +class FakeBindableIconsRegistry( + override val bindableIcons: List<BindableIcon>, +) : BindableIconsRegistry + +class FakeBindableIcon( + override val slot: String, + override val shouldBindIcon: Boolean = true, +) : BindableIcon { + // Track initialized so we can know that our icon was properly bound + var hasInitialized = false + + override val initializer = ModernStatusBarViewCreator { _ -> + hasInitialized = true + mock() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java index 0ff6f200d402..ca316230fb01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; @@ -108,7 +109,8 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { mock(TunerService.class), mock(DumpManager.class), mock(StatusBarIconList.class), - flags + flags, + mock(BindableIconsRegistry.class) ); iconController.addIconGroup(manager); 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 new file mode 100644 index 000000000000..a906a8953e02 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -0,0 +1,391 @@ +/* + * 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.data.prod + +import android.os.OutcomeReceiver +import android.os.Process +import android.telephony.satellite.NtnSignalStrength +import android.telephony.satellite.NtnSignalStrengthCallback +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN +import android.telephony.satellite.SatelliteManager.SatelliteException +import android.telephony.satellite.SatelliteStateCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +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 +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { + private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl + + @Mock private lateinit var satelliteManager: SatelliteManager + + private val systemClock = FakeSystemClock() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun nullSatelliteManager_usesDefaultValues() = + testScope.runTest { + setupDefaultRepo() + underTest = + DeviceBasedSatelliteRepositoryImpl( + Optional.empty(), + dispatcher, + testScope.backgroundScope, + systemClock, + ) + + val connectionState by collectLastValue(underTest.connectionState) + val strength by collectLastValue(underTest.signalStrength) + val allowed by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(connectionState).isEqualTo(SatelliteConnectionState.Off) + assertThat(strength).isEqualTo(0) + assertThat(allowed).isFalse() + } + + @Test + fun connectionState_mapsFromSatelliteModemState() = + testScope.runTest { + setupDefaultRepo() + val latest by collectLastValue(underTest.connectionState) + runCurrent() + val callback = + withArgCaptor<SatelliteStateCallback> { + verify(satelliteManager).registerForSatelliteModemStateChanged(any(), capture()) + } + + // Mapping from modem state to SatelliteConnectionState is rote, just run all of the + // possibilities here + + // Off states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF) + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNAVAILABLE) + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // On states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_IDLE) + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_LISTENING) + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_NOT_CONNECTED) + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + + // Connected states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_CONNECTED) + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING) + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_RETRYING) + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + + // Unknown states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNKNOWN) + assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown) + // Garbage value (for completeness' sake) + callback.onSatelliteModemStateChanged(123456) + assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown) + } + + @Test + fun signalStrength_readsSatelliteManagerState() = + testScope.runTest { + setupDefaultRepo() + val latest by collectLastValue(underTest.signalStrength) + runCurrent() + val callback = + withArgCaptor<NtnSignalStrengthCallback> { + verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture()) + } + + assertThat(latest).isNull() + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(1)) + assertThat(latest).isEqualTo(1) + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(2)) + assertThat(latest).isEqualTo(2) + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(3)) + assertThat(latest).isEqualTo(3) + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(4)) + assertThat(latest).isEqualTo(4) + } + + @Test + fun isSatelliteAllowed_readsSatelliteManagerState_enabled() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is allowed in this location + val allowed = true + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isTrue() + } + + @Test + fun isSatelliteAllowed_readsSatelliteManagerState_disabled() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is not allowed in this location + val allowed = false + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isFalse() + } + + @Test + fun isSatelliteAllowed_pollsOnTimeout() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is not allowed in this location + var allowed = false + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isFalse() + + // WHEN satellite becomes enabled + allowed = true + + // WHEN the timeout has not yet been reached + advanceTimeBy(POLLING_INTERVAL_MS / 2) + + // THEN the value is still false + assertThat(latest).isFalse() + + // WHEN time advances beyond the polling interval + advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1) + + // THEN then new value is emitted + assertThat(latest).isTrue() + } + + @Test + fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() = + testScope.runTest { + setupDefaultRepo() + // Use the old school launch/cancel so we can simulate subscribers arriving and leaving + + var latest: Boolean? = false + var job = + underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this) + + // GIVEN satellite is not allowed in this location + var allowed = false + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + assertThat(latest).isFalse() + + // WHEN satellite becomes enabled + allowed = true + + // WHEN the job is restarted + advanceTimeBy(POLLING_INTERVAL_MS / 2) + + job.cancel() + job = + underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this) + + // THEN the value is re-fetched + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isSatelliteAllowed_falseWhenErrorOccurs() = + testScope.runTest { + setupDefaultRepo() + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onError(SatelliteException(1 /* unused */)) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isFalse() + } + + @Test + fun satelliteNotSupported_listenersAreNotRegistered() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is not supported + setUpRepo( + uptime = MIN_UPTIME, + satMan = satelliteManager, + satelliteSupported = false, + ) + + // WHEN data is requested from the repo + val connectionState by collectLastValue(underTest.connectionState) + val signalStrength by collectLastValue(underTest.signalStrength) + + // THEN the manager is not asked for the information, and default values are returned + verify(satelliteManager, never()).registerForSatelliteModemStateChanged(any(), any()) + verify(satelliteManager, never()).registerForNtnSignalStrengthChanged(any(), any()) + } + + @Test + fun repoDoesNotCheckForSupportUntilMinUptime() = + testScope.runTest { + // GIVEN we init 100ms after sysui starts up + setUpRepo( + uptime = 100, + satMan = satelliteManager, + satelliteSupported = true, + ) + + // WHEN data is requested + val connectionState by collectLastValue(underTest.connectionState) + val signalStrength by collectLastValue(underTest.signalStrength) + + // THEN we have not yet talked to satellite manager, since we are well before MIN_UPTIME + Mockito.verifyZeroInteractions(satelliteManager) + + // WHEN enough time has passed + systemClock.advanceTime(MIN_UPTIME) + runCurrent() + + // THEN we finally register with the satellite manager + verify(satelliteManager).registerForSatelliteModemStateChanged(any(), any()) + } + + private fun setUpRepo( + uptime: Long = MIN_UPTIME, + satMan: SatelliteManager? = satelliteManager, + satelliteSupported: Boolean = true, + ) { + doAnswer { + val callback: OutcomeReceiver<Boolean, SatelliteException> = + it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> + callback.onResult(satelliteSupported) + } + .whenever(satelliteManager) + .requestIsSatelliteSupported(any(), any()) + + systemClock.setUptimeMillis(Process.getStartUptimeMillis() + uptime) + + underTest = + DeviceBasedSatelliteRepositoryImpl( + if (satMan != null) Optional.of(satMan) else Optional.empty(), + dispatcher, + testScope.backgroundScope, + systemClock, + ) + } + + // Set system time to MIN_UPTIME and create a repo with satellite supported + private fun setupDefaultRepo() { + setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt new file mode 100644 index 000000000000..5fa2d33c9de0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt @@ -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 com.android.systemui.statusbar.pipeline.satellite.data.prod + +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState.Off +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeDeviceBasedSatelliteRepository() : DeviceBasedSatelliteRepository { + override val connectionState = MutableStateFlow(Off) + + override val signalStrength = MutableStateFlow(0) + + override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt new file mode 100644 index 000000000000..e010b8672580 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt @@ -0,0 +1,322 @@ +/* + * 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.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG +import com.android.systemui.SysuiTestCase +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.shared.model.SatelliteConnectionState +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before + +@SmallTest +class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { + private lateinit var underTest: DeviceBasedSatelliteInteractor + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private val iconsInteractor = + FakeMobileIconsInteractor( + FakeMobileMappingsProxy(), + mock(), + ) + + private val repo = FakeDeviceBasedSatelliteRepository() + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + } + + @Test + fun isSatelliteAllowed_falseWhenNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.isSatelliteAllowed) + + // WHEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // THEN the interactor returns false due to the flag value + assertThat(latest).isFalse() + } + + @Test + fun isSatelliteAllowed_trueWhenAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.isSatelliteAllowed) + + // WHEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // THEN the interactor returns false due to the flag value + assertThat(latest).isTrue() + } + + @Test + fun isSatelliteAllowed_offWhenFlagIsOff() = + testScope.runTest { + // GIVEN feature is disabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.isSatelliteAllowed) + + // WHEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // THEN the interactor returns false due to the flag value + assertThat(latest).isFalse() + } + + @Test + fun connectionState_matchesRepositoryValue() = + testScope.runTest { + val latest by collectLastValue(underTest.connectionState) + + // Off + repo.connectionState.value = SatelliteConnectionState.Off + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // On + repo.connectionState.value = SatelliteConnectionState.On + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + + // Connected + repo.connectionState.value = SatelliteConnectionState.Connected + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + + // Unknown + repo.connectionState.value = SatelliteConnectionState.Unknown + assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown) + } + + @Test + fun connectionState_offWhenFeatureIsDisabled() = + testScope.runTest { + // GIVEN the flag is disabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.connectionState) + + // THEN the state is always Off, regardless of status in system_server + + // Off + repo.connectionState.value = SatelliteConnectionState.Off + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // On + repo.connectionState.value = SatelliteConnectionState.On + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // Connected + repo.connectionState.value = SatelliteConnectionState.Connected + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // Unknown + repo.connectionState.value = SatelliteConnectionState.Unknown + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + } + + @Test + fun signalStrength_matchesRepo() = + testScope.runTest { + val latest by collectLastValue(underTest.signalStrength) + + repo.signalStrength.value = 1 + assertThat(latest).isEqualTo(1) + + repo.signalStrength.value = 2 + assertThat(latest).isEqualTo(2) + + repo.signalStrength.value = 3 + assertThat(latest).isEqualTo(3) + + repo.signalStrength.value = 4 + assertThat(latest).isEqualTo(4) + } + + @Test + fun signalStrength_zeroWhenDisabled() = + testScope.runTest { + // GIVEN the flag is enabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.signalStrength) + + // THEN the value is always 0, regardless of what the system says + repo.signalStrength.value = 1 + assertThat(latest).isEqualTo(0) + + repo.signalStrength.value = 2 + assertThat(latest).isEqualTo(0) + + repo.signalStrength.value = 3 + assertThat(latest).isEqualTo(0) + + repo.signalStrength.value = 4 + assertThat(latest).isEqualTo(0) + } + + @Test + fun areAllConnectionsOutOfService_twoConnectionsOos_yes() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 2 connections + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2) + + // WHEN all of the connections are OOS + i1.isInService.value = false + i2.isInService.value = false + + // THEN the value is propagated to this interactor + assertThat(latest).isTrue() + } + + @Test + fun areAllConnectionsOutOfService_oneConnectionOos_yes() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 1 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + // WHEN all of the connections are OOS + i1.isInService.value = false + + // THEN the value is propagated to this interactor + assertThat(latest).isTrue() + } + + @Test + fun areAllConnectionsOutOfService_oneConnectionInService_no() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 1 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + // WHEN all of the connections are NOT OOS + i1.isInService.value = true + + // THEN the value is propagated to this interactor + assertThat(latest).isFalse() + } + + @Test + fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 2 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2) + + // WHEN at least 1 connection is NOT OOS. + i1.isInService.value = false + i2.isInService.value = true + + // THEN the value is propagated to this interactor + assertThat(latest).isFalse() + } + + @Test + fun areAllConnectionsOutOfService_twoConnectionsInService_no() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 2 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + // WHEN all connections are NOT OOS. + i1.isInService.value = true + i2.isInService.value = true + + // THEN the value is propagated to this interactor + assertThat(latest).isFalse() + } + + @Test + fun areAllConnectionsOutOfService_falseWhenFlagIsOff() = + testScope.runTest { + // GIVEN the flag is disabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN a condition that should return true (all conections OOS) + + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + i1.isInService.value = true + i2.isInService.value = true + + // THEN the value is still false, because the flag is off + assertThat(latest).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index 2f79955c4f5b..a5c766d82d00 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.policy; import static android.os.BatteryManager.EXTRA_PRESENT; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker; import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS; + import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -59,6 +61,7 @@ import org.mockito.MockitoSession; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -286,6 +289,24 @@ public class BatteryControllerTest extends SysuiTestCase { Assert.assertFalse(mBatteryController.isIncompatibleCharging()); } + @Test + public void callbackRemovedWhileDispatching_doesntCrash() { + final AtomicBoolean remove = new AtomicBoolean(false); + BatteryStateChangeCallback callback = new BatteryStateChangeCallback() { + @Override + public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { + if (remove.get()) { + mBatteryController.removeCallback(this); + } + } + }; + mBatteryController.addCallback(callback); + // Add another callback so the iteration continues + mBatteryController.addCallback(new BatteryStateChangeCallback() {}); + remove.set(true); + mBatteryController.fireBatteryLevelChanged(); + } + private void setupIncompatibleCharging() { final List<UsbPort> usbPorts = new ArrayList<>(); usbPorts.add(mUsbPort); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java index b8e4306a319d..68c1b8d3f44f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java @@ -127,6 +127,25 @@ public class CastControllerImplTest extends SysuiTestCase { } } + /** Regression test for b/317700495 */ + @Test + public void removeCallbackWhileIterating_doesntCrash() { + final AtomicBoolean remove = new AtomicBoolean(false); + Callback callback = new Callback() { + @Override + public void onCastDevicesChanged() { + if (remove.get()) { + mController.removeCallback(this); + } + } + }; + mController.addCallback(callback); + // Add another callback so the iteration continues + mController.addCallback(() -> {}); + remove.set(true); + mController.fireOnCastDevicesChanged(); + } + @Test public void hasConnectedCastDevice_connected() { CastController.CastDevice castDevice = new CastController.CastDevice(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt index db0029af4ee2..777fa2871a64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt @@ -26,6 +26,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dump.DumpManager import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.time.FakeSystemClock import java.util.concurrent.Executor @@ -128,11 +130,42 @@ class FlashlightControllerImplTest : SysuiTestCase() { verify(cameraManager).setTorchMode(id, enable) } + @Test + fun testCallbackRemovedWhileDispatching_doesntCrash() { + injectCamera() + var remove = false + val callback = object : FlashlightController.FlashlightListener { + override fun onFlashlightChanged(enabled: Boolean) { + if (remove) { + controller.removeCallback(this) + } + } + + override fun onFlashlightError() {} + + override fun onFlashlightAvailabilityChanged(available: Boolean) {} + } + controller.addCallback(callback) + controller.addCallback(object : FlashlightController.FlashlightListener { + override fun onFlashlightChanged(enabled: Boolean) {} + + override fun onFlashlightError() {} + + override fun onFlashlightAvailabilityChanged(available: Boolean) {} + }) + backgroundExecutor.runAllReady() + + val captor = argumentCaptor<CameraManager.TorchCallback>() + verify(cameraManager).registerTorchCallback(any(), capture(captor)) + remove = true + captor.value.onTorchModeChanged(ID, true) + } + private fun injectCamera( flash: Boolean = true, facing: Int = CameraCharacteristics.LENS_FACING_BACK ): String { - val cameraID = "ID" + val cameraID = ID val camera = CameraCharacteristics(CameraMetadataNative().apply { set(CameraCharacteristics.FLASH_INFO_AVAILABLE, flash) set(CameraCharacteristics.LENS_FACING, facing) @@ -141,4 +174,8 @@ class FlashlightControllerImplTest : SysuiTestCase() { `when`(cameraManager.getCameraCharacteristics(cameraID)).thenReturn(camera) return cameraID } + + companion object { + private const val ID = "ID" + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt index 89989ce81dd6..b03edaf8ebd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt @@ -27,6 +27,7 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -130,4 +131,61 @@ class SafetyControllerTest : SysuiTestCase() { controller.mPermControllerChangeReceiver.onReceive(context, testIntent) verify(listener, never()).onSafetyCenterEnableChanged(true) } + + @Test + fun listenerRemovedWhileDispatching_doesNotCrash() { + var remove = false + val callback = object : SafetyController.Listener { + override fun onSafetyCenterEnableChanged(isSafetyCenterEnabled: Boolean) { + if (remove) { + controller.removeCallback(this) + } + } + } + + controller.addCallback(callback) + controller.addCallback {} + + remove = true + + `when`(scm.isSafetyCenterEnabled).thenReturn(true) + val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED) + testIntent.data = Uri.parse("package:$TEST_PC_PKG") + controller.mPermControllerChangeReceiver.onReceive(context, testIntent) + } + + @Test + fun listenerRemovedWhileDispatching_otherCallbacksCalled() { + var remove = false + var called = false + + val callback1 = object : SafetyController.Listener { + override fun onSafetyCenterEnableChanged(isSafetyCenterEnabled: Boolean) { + if (remove) { + controller.removeCallback(this) + } + } + } + + val callback2 = object : SafetyController.Listener { + override fun onSafetyCenterEnableChanged(isSafetyCenterEnabled: Boolean) { + // When the first callback is removed, we track if this is called + if (remove) { + called = true + } + } + } + + controller.addCallback(callback1) + controller.addCallback(callback2) + + remove = true + + `when`(scm.isSafetyCenterEnabled).thenReturn(true) + val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED) + testIntent.data = Uri.parse("package:$TEST_PC_PKG") + controller.mPermControllerChangeReceiver.onReceive(context, testIntent) + + assertThat(called).isTrue() + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java index c35bc69bf15b..1dab84eb6e6a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java @@ -63,6 +63,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; @SmallTest @RunWith(AndroidJUnit4.class) @@ -217,6 +218,28 @@ public class SecurityControllerTest extends SysuiTestCase { ), any(NetworkCallback.class)); } + @Test + public void testRemoveCallbackWhileDispatch_doesntCrash() { + final AtomicBoolean remove = new AtomicBoolean(false); + SecurityController.SecurityControllerCallback callback = + new SecurityController.SecurityControllerCallback() { + @Override + public void onStateChanged() { + if (remove.get()) { + mSecurityController.removeCallback(this); + } + } + }; + mSecurityController.addCallback(callback); + // Add another callback so the iteration continues + mSecurityController.addCallback(() -> {}); + mBgExecutor.runAllReady(); + remove.set(true); + + mSecurityController.onUserSwitched(10); + mBgExecutor.runAllReady(); + } + /** * refresh CA certs by sending a user unlocked broadcast for the desired user */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java index 6825f650c421..f1a2c281595d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java @@ -47,6 +47,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @SmallTest @@ -174,4 +175,26 @@ public class ZenModeControllerImplTest extends SysuiTestCase { } } + + @Test + public void testCallbackRemovedWhileDispatching_doesntCrash() { + final AtomicBoolean remove = new AtomicBoolean(false); + mGlobalSettings.putInt(Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); + TestableLooper.get(this).processAllMessages(); + final ZenModeController.Callback callback = new ZenModeController.Callback() { + @Override + public void onZenChanged(int zen) { + if (remove.get()) { + mController.removeCallback(this); + } + } + }; + mController.addCallback(callback); + mController.addCallback(new ZenModeController.Callback() {}); + + remove.set(true); + + mGlobalSettings.putInt(Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_NO_INTERRUPTIONS); + TestableLooper.get(this).processAllMessages(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt index 4e61b89b9c3e..2bc05fcc8166 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProviderTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.unfold.progress +import android.os.Handler +import android.os.HandlerThread import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -24,6 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.TestUnfoldTransitionProvider import com.android.systemui.utils.os.FakeHandler import kotlin.test.Test +import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @@ -93,4 +96,13 @@ class MainThreadUnfoldTransitionProgressProviderTest : SysuiTestCase() { listener.assertNotStarted() } + + @Test + fun addCallback_fromBackgroundThread_succeeds() = runTest { + val bgHandler = Handler(HandlerThread("TestBgThread").apply { start() }.looper) + bgHandler.runWithScissors({ progressProvider.addCallback(listener) }, 5000L) + + wrappedProgressProvider.onTransitionStarted() + listener.assertStarted() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java index db0139c9b0d1..55c49ee4360d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java @@ -24,6 +24,7 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -41,6 +42,7 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { private static final int MAX_RETRIES = 5; private static final int RETRY_DELAY_MS = 1000; private static final int CONNECTION_MIN_DURATION_MS = 5000; + private static final String DUMPSYS_NAME = "dumpsys_name"; private FakeSystemClock mFakeClock = new FakeSystemClock(); private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock); @@ -49,8 +51,14 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { private ObservableServiceConnection<Proxy> mConnection; @Mock + private ObservableServiceConnection.Callback<Proxy> mConnectionCallback; + + @Mock private Observer mObserver; + @Mock + private DumpManager mDumpManager; + private static class Proxy { } @@ -63,6 +71,8 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { mConnectionManager = new PersistentConnectionManager<>( mFakeClock, mFakeExecutor, + mDumpManager, + DUMPSYS_NAME, mConnection, MAX_RETRIES, RETRY_DELAY_MS, @@ -154,4 +164,16 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { callbackCaptor.getValue().onSourceChanged(); verify(mConnection).bind(); } + + @Test + public void testAddConnectionCallback() { + mConnectionManager.addConnectionCallback(mConnectionCallback); + verify(mConnection).addCallback(mConnectionCallback); + } + + @Test + public void testRemoveConnectionCallback() { + mConnectionManager.removeConnectionCallback(mConnectionCallback); + verify(mConnection).removeCallback(mConnectionCallback); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt index 3fdeb302dc34..3b5ff38e3663 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.data.repository +import android.util.Size import com.android.systemui.biometrics.shared.model.DisplayRotation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,6 +30,9 @@ class FakeDisplayStateRepository : DisplayStateRepository { private val _currentRotation = MutableStateFlow<DisplayRotation>(DisplayRotation.ROTATION_0) override val currentRotation: StateFlow<DisplayRotation> = _currentRotation.asStateFlow() + private val _currentDisplaySize = MutableStateFlow<Size>(Size(0, 0)) + override val currentDisplaySize: StateFlow<Size> = _currentDisplaySize.asStateFlow() + override val isReverseDefaultRotation = false fun setIsInRearDisplayMode(isInRearDisplayMode: Boolean) { @@ -38,4 +42,8 @@ class FakeDisplayStateRepository : DisplayStateRepository { fun setCurrentRotation(currentRotation: DisplayRotation) { _currentRotation.value = currentRotation } + + fun setCurrentDisplaySize(size: Size) { + _currentDisplaySize.value = size + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt index 51ce9f00a709..77f501f550d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.data.repository +import android.graphics.Point import com.android.systemui.biometrics.shared.model.LockoutMode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -28,6 +29,10 @@ class FakeFacePropertyRepository : FacePropertyRepository { private val lockoutModesForUser = mutableMapOf<Int, LockoutMode>() + private val faceSensorLocation = MutableStateFlow<Point?>(null) + override val sensorLocation: StateFlow<Point?> + get() = faceSensorLocation + fun setLockoutMode(userId: Int, mode: LockoutMode) { lockoutModesForUser[userId] = mode } @@ -38,4 +43,8 @@ class FakeFacePropertyRepository : FacePropertyRepository { fun setSensorInfo(value: FaceSensorInfo?) { faceSensorInfo.value = value } + + fun setSensorLocation(value: Point?) { + faceSensorLocation.value = value + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt index 3ab1b6c11e02..3ea3ccfb2909 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt @@ -16,8 +16,24 @@ package com.android.systemui.communal.data.repository +import com.android.systemui.communal.data.model.CommunalMediaModel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeCommunalMediaRepository( - override val mediaPlaying: MutableStateFlow<Boolean> = MutableStateFlow(false) -) : CommunalMediaRepository +class FakeCommunalMediaRepository : CommunalMediaRepository { + private val _mediaModel = MutableStateFlow(CommunalMediaModel.INACTIVE) + + override val mediaModel: Flow<CommunalMediaModel> = _mediaModel + + fun mediaActive(timestamp: Long = 0L) { + _mediaModel.value = + CommunalMediaModel( + hasAnyMediaOrRecommendation = true, + createdTimestampMillis = timestamp, + ) + } + + fun mediaInactive() { + _mediaModel.value = CommunalMediaModel.INACTIVE + } +} 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 c85c27e277b4..e82cae45c8f0 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 @@ -9,6 +9,7 @@ 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 @@ -52,4 +53,12 @@ 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 c6f12e2014b3..397dc1a464bd 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 @@ -1,15 +1,38 @@ package com.android.systemui.communal.data.repository +import android.content.ComponentName import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch /** Fake implementation of [CommunalWidgetRepository] */ -class FakeCommunalWidgetRepository : CommunalWidgetRepository { +class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : + CommunalWidgetRepository { private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList()) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets + private val _widgetAdded = MutableSharedFlow<Int>() + val widgetAdded: Flow<Int> = _widgetAdded + + private var nextWidgetId = 1 fun setCommunalWidgets(inventory: List<CommunalWidgetContentModel>) { _communalWidgets.value = inventory } + + override fun addWidget( + provider: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) { + coroutineScope.launch { + val id = nextWidgetId++ + if (configureWidget.invoke(id)) { + _widgetAdded.emit(id) + } + } + } } 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 faacce64b2e4..eb287ee522c0 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 @@ -36,7 +36,8 @@ object CommunalInteractorFactory { fun create( testScope: TestScope = TestScope(), communalRepository: FakeCommunalRepository = FakeCommunalRepository(), - widgetRepository: FakeCommunalWidgetRepository = FakeCommunalWidgetRepository(), + widgetRepository: FakeCommunalWidgetRepository = + FakeCommunalWidgetRepository(testScope.backgroundScope), mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(), smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt index 788e3aa9c41a..1ffc9f4e30b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt @@ -15,8 +15,6 @@ */ package com.android.systemui.statusbar.notification.data -import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardStateRepositoryModule import dagger.Module -@Module(includes = [FakeNotificationsKeyguardStateRepositoryModule::class]) -object FakeStatusBarNotificationsDataLayerModule +@Module(includes = []) object FakeStatusBarNotificationsDataLayerModule diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt deleted file mode 100644 index 5d3cb4db9c7e..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.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.statusbar.notification.data.repository - -import com.android.systemui.dagger.SysUISingleton -import dagger.Binds -import dagger.Module -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -@SysUISingleton -class FakeNotificationsKeyguardViewStateRepository @Inject constructor() : - NotificationsKeyguardViewStateRepository { - private val _notificationsFullyHidden = MutableStateFlow(false) - override val areNotificationsFullyHidden: Flow<Boolean> = _notificationsFullyHidden - - private val _isPulseExpanding = MutableStateFlow(false) - override val isPulseExpanding: Flow<Boolean> = _isPulseExpanding - - fun setNotificationsFullyHidden(fullyHidden: Boolean) { - _notificationsFullyHidden.value = fullyHidden - } - - fun setPulseExpanding(expanding: Boolean) { - _isPulseExpanding.value = expanding - } -} - -@Module -interface FakeNotificationsKeyguardStateRepositoryModule { - @Binds - fun bindFake( - fake: FakeNotificationsKeyguardViewStateRepository - ): NotificationsKeyguardViewStateRepository -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt index f2b9da413c22..df7fd94d19b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt @@ -18,7 +18,5 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.kosmos.Kosmos -var Kosmos.notificationsKeyguardViewStateRepository: NotificationsKeyguardViewStateRepository by - Kosmos.Fixture { fakeNotificationsKeyguardViewStateRepository } -val Kosmos.fakeNotificationsKeyguardViewStateRepository by - Kosmos.Fixture { FakeNotificationsKeyguardViewStateRepository() } +val Kosmos.notificationsKeyguardViewStateRepository: NotificationsKeyguardViewStateRepository by + Kosmos.Fixture { NotificationsKeyguardViewStateRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt index 432464e86c3f..61a38b864c40 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt @@ -18,13 +18,11 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor val Kosmos.notificationsKeyguardInteractor by Fixture { NotificationsKeyguardInteractor( repository = notificationsKeyguardViewStateRepository, - backgroundDispatcher = testDispatcher, ) } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt index 9bdf3d5d5307..fdce147d229b 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/MainThreadUnfoldTransitionProgressProvider.kt @@ -24,12 +24,16 @@ import com.android.systemui.unfold.dagger.UnfoldMain import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import java.util.Collections.synchronizedMap /** * [UnfoldTransitionProgressProvider] that forwards all progress to the main thread handler. * * This is needed when progress are calculated in the background, but some listeners need the * callbacks in the main thread. + * + * Note that this class assumes that the root provider has thread safe callback registration, as + * they might be called from any thread. */ class MainThreadUnfoldTransitionProgressProvider @AssistedInject @@ -38,27 +42,20 @@ constructor( @Assisted private val rootProvider: UnfoldTransitionProgressProvider ) : UnfoldTransitionProgressProvider { - private val listenerMap = mutableMapOf<TransitionProgressListener, TransitionProgressListener>() + private val listenerMap: MutableMap<TransitionProgressListener, TransitionProgressListener> = + synchronizedMap(mutableMapOf()) override fun addCallback(listener: TransitionProgressListener) { - assertMainThread() val proxy = TransitionProgressListerProxy(listener) rootProvider.addCallback(proxy) listenerMap[listener] = proxy } override fun removeCallback(listener: TransitionProgressListener) { - assertMainThread() val proxy = listenerMap.remove(listener) ?: return rootProvider.removeCallback(proxy) } - private fun assertMainThread() { - check(mainHandler.looper.isCurrentThread) { - "Should be called from the main thread, but this is ${Thread.currentThread()}" - } - } - override fun destroy() { rootProvider.destroy() } diff --git a/packages/overlays/NoCutoutOverlay/res/values/config.xml b/packages/overlays/NoCutoutOverlay/res/values/config.xml index ed0340b11229..b44a153ae48c 100644 --- a/packages/overlays/NoCutoutOverlay/res/values/config.xml +++ b/packages/overlays/NoCutoutOverlay/res/values/config.xml @@ -20,10 +20,17 @@ black in software (to avoid aliasing or emulate a cutout that is not physically existent). --> <bool name="config_fillMainBuiltInDisplayCutout">false</bool> + <!-- Whether the display cutout region of the secondary built-in display should be forced to + black in software (to avoid aliasing or emulate a cutout that is not physically existent). + --> + <bool name="config_fillSecondaryBuiltInDisplayCutout">false</bool> <!-- If true, and there is a cutout on the main built in display, the cutout will be masked by shrinking the display such that it does not overlap the cutout area. --> <bool name="config_maskMainBuiltInDisplayCutout">true</bool> + <!-- If true, and there is a cutout on the secondary built in display, the cutout will be masked + by shrinking the display such that it does not overlap the cutout area. --> + <bool name="config_maskSecondaryBuiltInDisplayCutout">true</bool> <!-- Height of the status bar --> <dimen name="status_bar_height_portrait">28dp</dimen> diff --git a/proto/src/criticalevents/critical_event_log.proto b/proto/src/criticalevents/critical_event_log.proto index 9cda2672eab0..cffcd0941df8 100644 --- a/proto/src/criticalevents/critical_event_log.proto +++ b/proto/src/criticalevents/critical_event_log.proto @@ -60,8 +60,11 @@ message CriticalEventProto { JavaCrash java_crash = 5; NativeCrash native_crash = 6; SystemServerStarted system_server_started = 7; + InstallPackages install_packages = 8; } + message InstallPackages {} + message SystemServerStarted {} message Watchdog { diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 513c09587026..d96787480cfa 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -142,6 +142,11 @@ public class RavenwoodRule implements TestRule { Assume.assumeFalse(IS_UNDER_RAVENWOOD); } + // Stopgap for http://g/ravenwood/EPAD-N5ntxM + if (description.getMethodName().endsWith("$noRavenwood")) { + Assume.assumeFalse(IS_UNDER_RAVENWOOD); + } + RavenwoodRuleImpl.init(RavenwoodRule.this); try { base.evaluate(); diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java index 085c18622885..7abfecf0e424 100644 --- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java +++ b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java @@ -42,4 +42,9 @@ public class RavenwoodMinimumTest { public void testIgnored() { throw new RuntimeException("Shouldn't be executed under ravenwood"); } + + @Test + public void testIgnored$noRavenwood() { + throw new RuntimeException("Shouldn't be executed under ravenwood"); + } } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index 491ed22ce2c8..ab2546bab246 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -9,8 +9,10 @@ com.android.internal.os.Clock com.android.internal.os.LongArrayMultiStateCounter com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer com.android.internal.os.MonotonicClock +com.android.internal.os.PowerProfile com.android.internal.os.PowerStats com.android.internal.os.PowerStats$Descriptor +com.android.internal.power.ModemPowerProfile android.util.AtomicFile android.util.DataUnit @@ -32,11 +34,16 @@ android.util.SparseSetArray android.util.TimeUtils android.util.Xml +android.os.AggregateBatteryConsumer android.os.BatteryConsumer +android.os.BatteryStats android.os.BatteryStats$HistoryItem android.os.BatteryStats$HistoryStepDetails android.os.BatteryStats$HistoryTag +android.os.BatteryStats$LongCounter android.os.BatteryStats$ProcessStateChange +android.os.BatteryUsageStats +android.os.BatteryUsageStatsQuery android.os.Binder android.os.Binder$IdentitySupplier android.os.Broadcaster @@ -54,11 +61,14 @@ android.os.MessageQueue android.os.PackageTagsList android.os.Parcel android.os.Parcelable +android.os.PowerComponents android.os.Process android.os.ServiceSpecificException android.os.SystemClock android.os.ThreadLocalWorkSource android.os.TimestampedValue +android.os.UidBatteryConsumer +android.os.UidBatteryConsumer$Builder android.os.UserHandle android.os.WorkSource @@ -117,6 +127,7 @@ android.graphics.RectF android.content.ContentProvider com.android.server.LocalServices +com.android.server.power.stats.BatteryStatsImpl com.android.internal.util.BitUtils com.android.internal.util.BitwiseInputStream diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java index 553ba124402c..7fc1738f3172 100644 --- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java +++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java @@ -16,6 +16,7 @@ package com.android.server.autofill; +import static com.android.server.autofill.Session.REQUEST_ID_KEY; import static com.android.server.autofill.Session.SESSION_ID_KEY; import android.annotation.NonNull; @@ -107,15 +108,16 @@ final class SecondaryProviderHandler implements RemoteFillService.FillServiceCal mLastFlag = flag; if (mRemoteFillService != null && mRemoteFillService.isCredentialAutofillService()) { Slog.v(TAG, "About to call CredAutofill service as secondary provider"); - addSessionIdToClientState(pendingFillRequest, pendingInlineSuggestionsRequest, id); + addSessionIdAndRequestIdToClientState(pendingFillRequest, + pendingInlineSuggestionsRequest, id); mRemoteFillService.onFillCredentialRequest(pendingFillRequest, client); } else { mRemoteFillService.onFillRequest(pendingFillRequest); } } - private FillRequest addSessionIdToClientState(FillRequest pendingFillRequest, - InlineSuggestionsRequest pendingInlineSuggestionsRequest, int id) { + private FillRequest addSessionIdAndRequestIdToClientState(FillRequest pendingFillRequest, + InlineSuggestionsRequest pendingInlineSuggestionsRequest, int sessionId) { if (pendingFillRequest.getClientState() == null) { pendingFillRequest = new FillRequest(pendingFillRequest.getId(), pendingFillRequest.getFillContexts(), @@ -125,7 +127,8 @@ final class SecondaryProviderHandler implements RemoteFillService.FillServiceCal pendingInlineSuggestionsRequest, pendingFillRequest.getDelayedFillIntentSender()); } - pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, id); + pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, sessionId); + pendingFillRequest.getClientState().putInt(REQUEST_ID_KEY, pendingFillRequest.getId()); return pendingFillRequest; } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 007be05ff929..6a81425c1443 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -234,7 +234,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState new ComponentName("com.android.credentialmanager", "com.android.credentialmanager.autofill.CredentialAutofillService"); - static final String SESSION_ID_KEY = "session_id"; + static final String SESSION_ID_KEY = "autofill_session_id"; + static final String REQUEST_ID_KEY = "autofill_request_id"; final Object mLock; @@ -729,7 +730,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mPendingFillRequest.getFlags(), id, mClient); } else if (mRemoteFillService != null) { if (mIsPrimaryCredential) { - mPendingFillRequest = addSessionIdToClientState(mPendingFillRequest, + mPendingFillRequest = addSessionIdAndRequestIdToClientState(mPendingFillRequest, mPendingInlineSuggestionsRequest, id); mRemoteFillService.onFillCredentialRequest(mPendingFillRequest, mClient); } else { @@ -877,8 +878,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } - private FillRequest addSessionIdToClientState(FillRequest pendingFillRequest, - InlineSuggestionsRequest pendingInlineSuggestionsRequest, int id) { + private FillRequest addSessionIdAndRequestIdToClientState(FillRequest pendingFillRequest, + InlineSuggestionsRequest pendingInlineSuggestionsRequest, int sessionId) { if (pendingFillRequest.getClientState() == null) { pendingFillRequest = new FillRequest(pendingFillRequest.getId(), pendingFillRequest.getFillContexts(), @@ -888,7 +889,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState pendingInlineSuggestionsRequest, pendingFillRequest.getDelayedFillIntentSender()); } - pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, id); + pendingFillRequest.getClientState().putInt(SESSION_ID_KEY, sessionId); + pendingFillRequest.getClientState().putInt(REQUEST_ID_KEY, pendingFillRequest.getId()); return pendingFillRequest; } diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index a6ed8464128a..4b3772a7a54d 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java @@ -284,7 +284,7 @@ class AssociationRequestsProcessor { final AssociationInfo association = new AssociationInfo(id, userId, packageName, /* tag */ null, macAddress, displayName, deviceProfile, associatedDevice, selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false, - timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); + /* pending */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); // Add role holder for association (if specified) and add new association to store. maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java index a7dbd1c15aec..e4cc1f8949b5 100644 --- a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java +++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java @@ -18,6 +18,8 @@ package com.android.server.companion; import static android.os.UserHandle.getCallingUserId; +import static com.android.server.companion.CompanionDeviceManagerService.PerUserAssociationSet; + import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.UserIdInt; @@ -26,9 +28,11 @@ import android.companion.Flags; import android.companion.datatransfer.SystemDataTransferRequest; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManagerInternal; +import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; @@ -58,6 +62,14 @@ class BackupRestoreProcessor { @NonNull private final AssociationRequestsProcessor mAssociationRequestsProcessor; + /** + * A structure that consists of a set of restored associations that are pending corresponding + * companion app to be installed. + */ + @GuardedBy("mAssociationsPendingAppInstall") + private final PerUserAssociationSet mAssociationsPendingAppInstall = + new PerUserAssociationSet(); + BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service, @NonNull AssociationStoreImpl associationStore, @NonNull PersistentDataStore persistentStore, @@ -124,7 +136,7 @@ class BackupRestoreProcessor { byte[] requestsPayload = new byte[buffer.getInt()]; buffer.get(requestsPayload); List<SystemDataTransferRequest> restoredRequestsForUser = - mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload); + mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload, userId); // Get a list of installed packages ahead of time. List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications( @@ -170,7 +182,7 @@ class BackupRestoreProcessor { mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation, null, null); } else { - // TODO(b/314992577): Check if package is installed before granting + addToPendingAppInstall(newAssociation); } // Re-map restored system data transfer requests to newly created associations @@ -185,6 +197,30 @@ class BackupRestoreProcessor { mService.persistStateForUser(userId); } + void addToPendingAppInstall(@NonNull AssociationInfo association) { + association = (new AssociationInfo.Builder(association)) + .setPending(true) + .build(); + + synchronized (mAssociationsPendingAppInstall) { + mAssociationsPendingAppInstall.forUser(association.getUserId()).add(association); + } + } + + void removeFromPendingAppInstall(@NonNull AssociationInfo association) { + synchronized (mAssociationsPendingAppInstall) { + mAssociationsPendingAppInstall.forUser(association.getUserId()).remove(association); + } + } + + @NonNull + Set<AssociationInfo> getAssociationsPendingAppInstallForUser(@UserIdInt int userId) { + synchronized (mAssociationsPendingAppInstall) { + // Return a copy. + return new ArraySet<>(mAssociationsPendingAppInstall.forUser(userId)); + } + } + /** * Detects and handles collision between restored association and local association. Returns * true if there has been a collision and false otherwise. diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 858887ae20c6..056ec895821d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -287,7 +287,9 @@ public class CompanionDeviceManagerService extends SystemService { final Set<Integer> usersToPersistStateFor = new ArraySet<>(); for (AssociationInfo association : allAssociations) { - if (!association.isRevoked()) { + if (association.isPending()) { + mBackupRestoreProcessor.addToPendingAppInstall(association); + } else if (!association.isRevoked()) { activeAssociations.add(association); } else if (maybeRemoveRoleHolderForAssociation(association)) { // Nothing more to do here, but we'll need to persist all the associations to the @@ -514,6 +516,9 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore.getAssociationsForUser(userId)); // ... and add the revoked (removed) association, that are yet to be permanently removed. allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId)); + // ... and add the restored associations that are pending missing package installation. + allAssociations.addAll(mBackupRestoreProcessor + .getAssociationsPendingAppInstallForUser(userId)); final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId); @@ -583,7 +588,19 @@ public class CompanionDeviceManagerService extends SystemService { private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) { if (DEBUG) Log.i(TAG, "onPackageAddedInternal() u" + userId + "/" + packageName); - // TODO(b/314992577): Retroactively grant roles for restored associations + + Set<AssociationInfo> associationsPendingAppInstall = mBackupRestoreProcessor + .getAssociationsPendingAppInstallForUser(userId); + for (AssociationInfo association : associationsPendingAppInstall) { + if (!packageName.equals(association.getPackageName())) continue; + + AssociationInfo newAssociation = new AssociationInfo.Builder(association) + .setPending(false) + .build(); + mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation, + null, null); + mBackupRestoreProcessor.removeFromPendingAppInstall(association); + } } // Revoke associations if the selfManaged companion device does not connect for 3 months. @@ -1152,6 +1169,15 @@ public class CompanionDeviceManagerService extends SystemService { usedIds.put(it.getId(), true); } + // Some IDs may be reserved by associations that aren't stored yet due to missing + // package after a backup restoration. We don't want the ID to have been taken by + // another association by the time when it is activated from the package installation. + final Set<AssociationInfo> pendingAssociations = mBackupRestoreProcessor + .getAssociationsPendingAppInstallForUser(userId); + for (AssociationInfo it: pendingAssociations) { + usedIds.put(it.getId(), true); + } + // Second: collect all IDs that have been previously used for this package (and user). final Set<Integer> previouslyUsedIds = getPreviouslyUsedIdsForPackageLocked(userId, packageName); @@ -1718,7 +1744,7 @@ public class CompanionDeviceManagerService extends SystemService { } } - private static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> { + static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> { @Override protected @NonNull Set<AssociationInfo> create(int userId) { return new ArraySet<>(); diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java index dbaf7e85b7fa..1ebe65c6aa5f 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java @@ -189,6 +189,7 @@ final class PersistentDataStore { private static final String XML_ATTR_SELF_MANAGED = "self_managed"; private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; private static final String XML_ATTR_REVOKED = "revoked"; + private static final String XML_ATTR_PENDING = "pending"; private static final String XML_ATTR_TIME_APPROVED = "time_approved"; private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; private static final String XML_ATTR_SYSTEM_DATA_SYNC_FLAGS = "system_data_sync_flags"; @@ -464,8 +465,8 @@ final class PersistentDataStore { out.add(new AssociationInfo(associationId, userId, appPackage, tag, MacAddress.fromString(deviceAddress), null, profile, null, - /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved, - Long.MAX_VALUE, /* systemDataSyncFlags */ 0)); + /* managedByCompanionApp */ false, notify, /* revoked */ false, /* pending */ false, + timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0)); } private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, @@ -496,6 +497,7 @@ final class PersistentDataStore { final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false); + final boolean pending = readBooleanAttribute(parser, XML_ATTR_PENDING, false); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); final long lastTimeConnected = readLongAttribute( parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); @@ -504,7 +506,7 @@ final class PersistentDataStore { final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, appPackage, tag, macAddress, displayName, profile, selfManaged, notify, revoked, - timeApproved, lastTimeConnected, systemDataSyncFlags); + pending, timeApproved, lastTimeConnected, systemDataSyncFlags); if (associationInfo != null) { out.add(associationInfo); } @@ -558,8 +560,8 @@ final class PersistentDataStore { writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged()); writeBooleanAttribute( serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby()); - writeBooleanAttribute( - serializer, XML_ATTR_REVOKED, a.isRevoked()); + writeBooleanAttribute(serializer, XML_ATTR_REVOKED, a.isRevoked()); + writeBooleanAttribute(serializer, XML_ATTR_PENDING, a.isPending()); writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs()); writeLongAttribute( serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); @@ -603,14 +605,14 @@ final class PersistentDataStore { @UserIdInt int userId, @NonNull String appPackage, @Nullable String tag, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged, boolean notify, boolean revoked, - long timeApproved, long lastTimeConnected, int systemDataSyncFlags) { + boolean pending, long timeApproved, long lastTimeConnected, int systemDataSyncFlags) { AssociationInfo associationInfo = null; try { // We do not persist AssociatedDevice, which means that AssociationInfo retrieved from // datastore is not guaranteed to be identical to the one from initial association. associationInfo = new AssociationInfo(associationId, userId, appPackage, tag, macAddress, displayName, profile, null, selfManaged, notify, - revoked, timeApproved, lastTimeConnected, systemDataSyncFlags); + revoked, pending, timeApproved, lastTimeConnected, systemDataSyncFlags); } catch (Exception e) { if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); } diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java index 8fe04547a9ec..51c5fd69cdf2 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java @@ -69,7 +69,6 @@ import java.util.concurrent.TimeoutException; * <request * association_id="1" * data_type="1" - * user_id="12" * is_user_consented="true" * </request> * </requests> @@ -86,7 +85,6 @@ public class SystemDataTransferRequestStore { private static final String XML_ATTR_ASSOCIATION_ID = "association_id"; private static final String XML_ATTR_DATA_TYPE = "data_type"; - private static final String XML_ATTR_USER_ID = "user_id"; private static final String XML_ATTR_IS_USER_CONSENTED = "is_user_consented"; private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds @@ -169,12 +167,12 @@ public class SystemDataTransferRequestStore { * Parse the byte array containing XML information of system data transfer requests into * an array list of requests. */ - public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload) { + public List<SystemDataTransferRequest> readRequestsFromPayload(byte[] payload, int userId) { try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { final TypedXmlPullParser parser = Xml.resolvePullParser(in); XmlUtils.beginDocument(parser, XML_TAG_REQUESTS); - return readRequestsFromXml(parser); + return readRequestsFromXml(parser, userId); } catch (XmlPullParserException | IOException e) { Slog.e(LOG_TAG, "Error while reading requests file", e); return new ArrayList<>(); @@ -226,7 +224,7 @@ public class SystemDataTransferRequestStore { final TypedXmlPullParser parser = Xml.resolvePullParser(in); XmlUtils.beginDocument(parser, XML_TAG_REQUESTS); - return readRequestsFromXml(parser); + return readRequestsFromXml(parser, userId); } catch (XmlPullParserException | IOException e) { Slog.e(LOG_TAG, "Error while reading requests file", e); return new ArrayList<>(); @@ -236,7 +234,8 @@ public class SystemDataTransferRequestStore { @NonNull private ArrayList<SystemDataTransferRequest> readRequestsFromXml( - @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException { + @NonNull TypedXmlPullParser parser, int userId) + throws XmlPullParserException, IOException { if (!isStartOfTag(parser, XML_TAG_REQUESTS)) { throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_REQUESTS); } @@ -249,14 +248,15 @@ public class SystemDataTransferRequestStore { break; } if (isStartOfTag(parser, XML_TAG_REQUEST)) { - requests.add(readRequestFromXml(parser)); + requests.add(readRequestFromXml(parser, userId)); } } return requests; } - private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser) + private SystemDataTransferRequest readRequestFromXml(@NonNull TypedXmlPullParser parser, + int userId) throws XmlPullParserException, IOException { if (!isStartOfTag(parser, XML_TAG_REQUEST)) { throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_REQUEST); @@ -264,7 +264,6 @@ public class SystemDataTransferRequestStore { final int associationId = readIntAttribute(parser, XML_ATTR_ASSOCIATION_ID); final int dataType = readIntAttribute(parser, XML_ATTR_DATA_TYPE); - final int userId = readIntAttribute(parser, XML_ATTR_USER_ID); final boolean isUserConsented = readBooleanAttribute(parser, XML_ATTR_IS_USER_CONSENTED); switch (dataType) { @@ -321,7 +320,6 @@ public class SystemDataTransferRequestStore { writeIntAttribute(serializer, XML_ATTR_ASSOCIATION_ID, request.getAssociationId()); writeIntAttribute(serializer, XML_ATTR_DATA_TYPE, request.getDataType()); - writeIntAttribute(serializer, XML_ATTR_USER_ID, request.getUserId()); writeBooleanAttribute(serializer, XML_ATTR_IS_USER_CONSENTED, request.isUserConsented()); serializer.endTag(null, XML_TAG_REQUEST); diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java index d089b05238e4..2f9b6a56e316 100644 --- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java +++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java @@ -55,9 +55,7 @@ public final class VirtualCameraController implements IBinder.DeathRecipient { @GuardedBy("mCameras") private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>(); - public VirtualCameraController() { - connectVirtualCameraService(); - } + public VirtualCameraController() {} @VisibleForTesting VirtualCameraController(IVirtualCameraService virtualCameraService) { diff --git a/services/core/Android.bp b/services/core/Android.bp index dd001ec7da27..a3fc3bf5ec72 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -203,6 +203,7 @@ java_library_static { "com_android_wm_shell_flags_lib", "com.android.server.utils_aconfig-java", "service-jobscheduler-deviceidle.flags-aconfig-java", + "policy_flags_lib", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 9f279b1ba3fe..329aac6f3a6a 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -48,6 +48,8 @@ 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; @@ -60,11 +62,14 @@ 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 @@ -332,12 +337,12 @@ public class BootReceiver extends BroadcastReceiver { * * @param ctx Context * @param tombstone path to the tombstone - * @param proto whether the tombstone is stored as proto + * @param tombstoneProto the parsed proto tombstone * @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, boolean proto, String processName, + Context ctx, File tombstone, Tombstone tombstoneProto, String processName, ReentrantLock tmpFileLock) { final DropBoxManager db = ctx.getSystemService(DropBoxManager.class); if (db == null) { @@ -347,31 +352,33 @@ public class BootReceiver extends BroadcastReceiver { // Check if we should rate limit and abort early if needed. DropboxRateLimiter.RateLimitResult rateLimitResult = - sDropboxRateLimiter.shouldRateLimit( - proto ? TAG_TOMBSTONE_PROTO_WITH_HEADERS : TAG_TOMBSTONE, processName); + sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName); if (rateLimitResult.shouldRateLimit()) return; HashMap<String, Long> timestamps = readTimestamps(); try { - 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(); - } + // 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(); } - } 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); @@ -380,11 +387,8 @@ public class BootReceiver extends BroadcastReceiver { } private static void addAugmentedProtoToDropbox( - File tombstone, DropBoxManager db, + File tombstone, byte[] tombstoneBytes, 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( @@ -417,6 +421,8 @@ 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, @@ -434,6 +440,31 @@ 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/OWNERS b/services/core/java/com/android/server/OWNERS index e289a56f5dc5..e923e30aa1c6 100644 --- a/services/core/java/com/android/server/OWNERS +++ b/services/core/java/com/android/server/OWNERS @@ -43,3 +43,6 @@ per-file SystemTimeZone.java = file:/services/core/java/com/android/server/timez per-file TelephonyRegistry.java = file:/telephony/OWNERS per-file UiModeManagerService.java = file:/packages/SystemUI/OWNERS per-file VcnManagementService.java = file:/services/core/java/com/android/server/vcn/OWNERS + +# SystemConfig +per-file SystemConfig.java = file:/PACKAGE_MANAGER_OWNERS diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java new file mode 100644 index 000000000000..70bd4b328b43 --- /dev/null +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -0,0 +1,188 @@ +/* + * 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.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.media.projection.MediaProjectionInfo; +import android.media.projection.MediaProjectionManager; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationListenerService.RankingMap; +import android.service.notification.StatusBarNotification; +import android.util.ArraySet; +import android.util.Log; + +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; + +/** + * Service that monitors for notifications with sensitive content and protects content from screen + * sharing + */ +public final class SensitiveContentProtectionManagerService extends SystemService { + private static final String TAG = "SensitiveContentProtect"; + private static final boolean DEBUG = false; + + @VisibleForTesting + NotificationListener mNotificationListener; + private @Nullable MediaProjectionManager mProjectionManager; + private @Nullable WindowManagerInternal mWindowManager; + + private final MediaProjectionManager.Callback mProjectionCallback = + new MediaProjectionManager.Callback() { + @Override + public void onStart(MediaProjectionInfo info) { + if (DEBUG) Log.d(TAG, "onStart projection: " + info); + onProjectionStart(); + } + + @Override + public void onStop(MediaProjectionInfo info) { + if (DEBUG) Log.d(TAG, "onStop projection: " + info); + onProjectionEnd(); + } + }; + + public SensitiveContentProtectionManagerService(@NonNull Context context) { + super(context); + mNotificationListener = new NotificationListener(); + } + + @Override + public void onStart() {} + + @Override + public void onBootPhase(int phase) { + if (phase != SystemService.PHASE_BOOT_COMPLETED) { + return; + } + + if (DEBUG) Log.d(TAG, "onBootPhase - PHASE_BOOT_COMPLETED"); + + init(getContext().getSystemService(MediaProjectionManager.class), + LocalServices.getService(WindowManagerInternal.class)); + } + + @VisibleForTesting + void init(MediaProjectionManager projectionManager, + WindowManagerInternal windowManager) { + if (DEBUG) Log.d(TAG, "init"); + + checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager"); + checkNotNull(windowManager, "Failed to get valid WindowManagerInternal"); + + mProjectionManager = projectionManager; + mWindowManager = windowManager; + + // TODO(b/317250444): use MediaProjectionManagerService directly, reduces unnecessary + // handler, delegate, and binder death recipient + mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper())); + + try { + mNotificationListener.registerAsSystemService(getContext(), + new ComponentName(getContext(), NotificationListener.class), + UserHandle.USER_ALL); + } catch (RemoteException e) { + // Intra-process call, should never happen. + } + } + + /** Cleanup any callbacks and listeners */ + @VisibleForTesting + void onDestroy() { + if (mProjectionManager != null) { + mProjectionManager.removeCallback(mProjectionCallback); + } + + try { + mNotificationListener.unregisterAsSystemService(); + } catch (RemoteException e) { + // Intra-process call, should never happen. + } + + if (mWindowManager != null) { + onProjectionEnd(); + } + } + + 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]; + } + + RankingMap rankingMap; + try { + rankingMap = mNotificationListener.getCurrentRanking(); + } catch (SecurityException e) { + Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e); + rankingMap = null; + } + + // notify windowmanager of any currently posted sensitive content notifications + Set<PackageInfo> packageInfos = getSensitivePackagesFromNotifications( + notifications, + rankingMap); + + mWindowManager.setShouldBlockScreenCaptureForApp(packageInfos); + } + + private void onProjectionEnd() { + // notify windowmanager to clear any sensitive notifications observed during projection + // session + mWindowManager.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + private Set<PackageInfo> getSensitivePackagesFromNotifications( + StatusBarNotification[] notifications, RankingMap rankingMap) { + if (rankingMap == null) { + Log.w(TAG, "Ranking map not initialized."); + return Collections.emptySet(); + } + + 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()); + sensitivePackages.add(info); + } + } + return sensitivePackages; + } + + // TODO(b/317251408): add trigger that updates on onNotificationPosted, + // onNotificationRankingUpdate and onListenerConnected + @VisibleForTesting + static class NotificationListener extends NotificationListenerService {} +} diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 19a9239752cd..7a4ac6ac4500 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1356,8 +1356,8 @@ class StorageManagerService extends IStorageManager.Stub final int flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE; for (UserInfo user : users) { - prepareUserStorageInternal(fromVolumeUuid, user.id, user.serialNumber, flags); - prepareUserStorageInternal(toVolumeUuid, user.id, user.serialNumber, flags); + prepareUserStorageInternal(fromVolumeUuid, user.id, flags); + prepareUserStorageInternal(toVolumeUuid, user.id, flags); } } @@ -3231,7 +3231,7 @@ class StorageManagerService extends IStorageManager.Stub @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL) @Override - public void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) { + public void createUserStorageKeys(int userId, boolean ephemeral) { super.createUserStorageKeys_enforcePermission(); @@ -3276,8 +3276,7 @@ class StorageManagerService extends IStorageManager.Stub /* Only for use by LockSettingsService */ @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL) @Override - public void unlockCeStorage(@UserIdInt int userId, int serialNumber, byte[] secret) - throws RemoteException { + public void unlockCeStorage(@UserIdInt int userId, byte[] secret) throws RemoteException { super.unlockCeStorage_enforcePermission(); if (StorageManager.isFileEncrypted()) { @@ -3348,25 +3347,25 @@ class StorageManagerService extends IStorageManager.Stub continue; } - prepareUserStorageInternal(vol.fsUuid, user.id, user.serialNumber, flags); + prepareUserStorageInternal(vol.fsUuid, user.id, flags); } } @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL) @Override - public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) { + public void prepareUserStorage(String volumeUuid, int userId, int flags) { super.prepareUserStorage_enforcePermission(); try { - prepareUserStorageInternal(volumeUuid, userId, serialNumber, flags); + prepareUserStorageInternal(volumeUuid, userId, flags); } catch (Exception e) { throw new RuntimeException(e); } } - private void prepareUserStorageInternal(String volumeUuid, int userId, int serialNumber, - int flags) throws Exception { + private void prepareUserStorageInternal(String volumeUuid, int userId, int flags) + throws Exception { try { mVold.prepareUserStorage(volumeUuid, userId, flags); // After preparing user storage, we should check if we should mount data mirror again, diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index 40b29d7b09d5..3483c1a1404a 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -315,6 +315,11 @@ public class SystemConfig { private final ArraySet<String> mBugreportWhitelistedPackages = new ArraySet<>(); private final ArraySet<String> mAppDataIsolationWhitelistedApps = new ArraySet<>(); + // These packages will be set as 'prevent disable', where they are no longer possible + // for the end user to disable via settings. This flag should only be used for packages + // which meet the 'force or keep enabled apps' policy. + private final ArrayList<String> mPreventUserDisablePackages = new ArrayList<>(); + // Map of packagesNames to userTypes. Stored temporarily until cleared by UserManagerService(). private ArrayMap<String, Set<String>> mPackageToUserTypeWhitelist = new ArrayMap<>(); private ArrayMap<String, Set<String>> mPackageToUserTypeBlacklist = new ArrayMap<>(); @@ -504,6 +509,10 @@ public class SystemConfig { return mAppDataIsolationWhitelistedApps; } + public @NonNull ArrayList<String> getPreventUserDisablePackages() { + return mPreventUserDisablePackages; + } + /** * Gets map of packagesNames to userTypes, dictating on which user types each package should be * initially installed, and then removes this map from SystemConfig. @@ -1309,6 +1318,16 @@ public class SystemConfig { } XmlUtils.skipCurrentTag(parser); } break; + case "prevent-disable": { + String pkgname = parser.getAttributeValue(null, "package"); + if (pkgname == null) { + Slog.w(TAG, "<" + name + "> without package in " + permFile + + " at " + parser.getPositionDescription()); + } else { + mPreventUserDisablePackages.add(pkgname); + } + XmlUtils.skipCurrentTag(parser); + } break; case "install-in-user-type": { // NB: We allow any directory permission to declare install-in-user-type. readInstallInUserType(parser, diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 85abf87b4b2e..130a7333959d 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -3152,6 +3152,7 @@ public class AccountManagerService new AccountAuthenticatorResponse(this), authTokenType, true); + mCanStartAccountManagerActivity = true; Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); onResult(bundle); @@ -4933,6 +4934,7 @@ public class AccountManagerService IAccountAuthenticator mAuthenticator = null; private final boolean mStripAuthTokenFromResult; + protected boolean mCanStartAccountManagerActivity = false; protected final UserAccounts mAccounts; public Session(UserAccounts accounts, IAccountManagerResponse response, String accountType, @@ -5068,9 +5070,13 @@ public class AccountManagerService private boolean isExportedSystemActivity(ActivityInfo activityInfo) { String className = activityInfo.name; - return "android".equals(activityInfo.packageName) && - (GrantCredentialsPermissionActivity.class.getName().equals(className) - || CantAddAccountActivity.class.getName().equals(className)); + if (!"android".equals(activityInfo.packageName)) { + return false; + + } + return (mCanStartAccountManagerActivity + && GrantCredentialsPermissionActivity.class.getName().equals(className)) + || CantAddAccountActivity.class.getName().equals(className); } private void close() { diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 72e62c37106d..8ad60e6a0782 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -243,7 +243,7 @@ final class ActivityManagerConstants extends ContentObserver { /** * The default value to {@link #KEY_ENABLE_NEW_OOMADJ}. */ - private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = false; + private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = Flags.oomadjusterCorrectnessRewrite(); /** * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED} diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index f49e25af79d3..ef7a0e058db0 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1385,6 +1385,8 @@ public class OomAdjuster { break; } + // TODO: b/319163103 - limit isolated/sandbox trimming to just the processes + // evaluated in the current update. if (app.isolated && psr.numberOfRunningServices() <= 0 && app.getIsolatedEntryPoint() == null) { // If this is an isolated process, there are no services diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index 7cc7c517fa9c..5a3fbe9a66ac 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -724,24 +724,13 @@ public class OomAdjusterModernImpl extends OomAdjuster { if (fullUpdate) { assignCachedAdjIfNecessary(mProcessList.getLruProcessesLOSP()); - postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime); } else { activeProcesses.clear(); activeProcesses.addAll(targetProcesses); assignCachedAdjIfNecessary(activeProcesses); - - for (int i = activeUids.size() - 1; i >= 0; i--) { - final UidRecord uidRec = activeUids.valueAt(i); - uidRec.forEachProcess(this::updateAppUidRecIfNecessaryLSP); - } - updateUidsLSP(activeUids, nowElapsed); - - for (int i = 0, size = targetProcesses.size(); i < size; i++) { - applyOomAdjLSP(targetProcesses.valueAt(i), false, now, nowElapsed, oomAdjReason); - } - activeProcesses.clear(); } + postUpdateOomAdjInnerLSP(oomAdjReason, activeUids, now, nowElapsed, oldTime); targetProcesses.clear(); if (startProfiling) { diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 08b129eeb8e5..2771572edb01 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -39,7 +39,7 @@ import android.app.PendingIntent; import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; -import android.compat.annotation.Disabled; +import android.compat.annotation.EnabledAfter; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -49,6 +49,7 @@ import android.content.pm.ServiceInfo; import android.net.Uri; import android.os.Binder; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.IBinder; import android.os.PowerExemptionManager; import android.os.SystemClock; @@ -94,16 +95,14 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN * (See also android.app.ForegroundServiceTypePolicy) */ @ChangeId - // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) - @Disabled + @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) static final long USE_NEW_WIU_LOGIC_FOR_START = 311208629L; /** * Compat ID to enable the new FGS start logic, for capability calculation. */ @ChangeId - // Always enabled - @Disabled + @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) static final long USE_NEW_WIU_LOGIC_FOR_CAPABILITIES = 313677553L; /** @@ -111,8 +110,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN * the background. */ @ChangeId - // @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) - @Disabled + @EnabledAfter(targetSdkVersion = VERSION_CODES.UPSIDE_DOWN_CAKE) static final long USE_NEW_BFSL_LOGIC = 311208749L; final ActivityManagerService ams; diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index e7e721b3ec55..9db5d0a99480 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -176,6 +176,7 @@ public class SettingsToPropertiesMapper { "system_performance", "system_sw_touch", "system_sw_usb", + "statsd", "test_suites", "text", "threadnetwork", diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a1b6f297f287..91d533c73b5d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -13591,6 +13591,46 @@ public class AudioService extends IAudioService.Stub } } + + /** + * @see AudioManager#shouldNotificationSoundPlay(AudioAttributes) + */ + @android.annotation.EnforcePermission( + android.Manifest.permission.QUERY_AUDIO_STATE) + public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) { + super.shouldNotificationSoundPlay_enforcePermission(); + Objects.requireNonNull(aa); + + // don't play notifications if the stream volume associated with the + // AudioAttributes of the notification record is 0 (non-zero volume implies + // not silenced by SILENT or VIBRATE ringer mode) + final int stream = AudioAttributes.toLegacyStreamType(aa); + final boolean mutingFromVolume = getStreamVolume(stream) == 0; + if (mutingFromVolume) { + if (DEBUG_VOL) { + Slog.d(TAG, "notification should not play due to muted stream " + stream); + } + return false; + } + + // don't play notifications if there is a user of GAIN_TRANSIENT_EXCLUSIVE audio focus + // and the focus owner is recording + final int uid = mMediaFocusControl.getExclusiveFocusOwnerUid(); + if (uid == -1) { // return value is -1 if focus isn't GAIN_TRANSIENT_EXCLUSIVE + return true; + } + // is the owner of GAIN_TRANSIENT_EXCLUSIVE focus also recording? + final boolean mutingFromFocusAndRecording = mRecordMonitor.isRecordingActiveForUid(uid); + if (mutingFromFocusAndRecording) { + if (DEBUG_VOL) { + Slog.d(TAG, "notification should not play due to exclusive focus owner recording " + + " uid:" + uid); + } + return false; + } + return true; + } + //====================== // Audioserver state dispatch //====================== diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java index 0df0006c7be3..1376bde2fb71 100644 --- a/services/core/java/com/android/server/audio/MediaFocusControl.java +++ b/services/core/java/com/android/server/audio/MediaFocusControl.java @@ -297,6 +297,23 @@ public class MediaFocusControl implements PlayerFocusEnforcer { } /** + * Return the UID of the focus owner that has focus with exclusive focus gain + * @return -1 if nobody has exclusive focus, the UID of the owner otherwise + */ + protected int getExclusiveFocusOwnerUid() { + synchronized (mAudioFocusLock) { + if (mFocusStack.empty()) { + return -1; + } + final FocusRequester owner = mFocusStack.peek(); + if (owner.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) { + return -1; + } + return owner.getClientUid(); + } + } + + /** * Send AUDIOFOCUS_LOSS to a specific stack entry. * Note this method is supporting an external API, and is restricted to LOSS in order to * prevent allowing the stack to be in an invalid state (e.g. entry inside stack has focus) diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index c72632fb367d..c2bc1e4f6be2 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -893,7 +893,7 @@ public class SoundDoseHelper { if (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC && safeDevicesContains(device)) { soundDose.updateAttenuation( - AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC, + -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC, (newIndex + 5) / 10, device), device); } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 89b638be3500..89e08c165373 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors; +import static com.android.server.biometrics.sensors.BiometricSchedulerOperation.STATE_STARTED; + import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; @@ -28,6 +30,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -35,6 +38,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.modules.expresslog.Counter; import com.android.server.biometrics.BiometricSchedulerProto; import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import java.io.PrintWriter; @@ -48,6 +52,7 @@ import java.util.Deque; import java.util.List; import java.util.Locale; import java.util.function.Consumer; +import java.util.function.Supplier; /** * A scheduler for biometric HAL operations. Maintains a queue of {@link BaseClientMonitor} @@ -56,11 +61,16 @@ import java.util.function.Consumer; * * We currently assume (and require) that each biometric sensor have its own instance of a * {@link BiometricScheduler}. + * + * @param <T> Hal instance for starting the user. + * @param <U> Session associated with the current user id. + * + * TODO: (b/304604965) Update thread annotation when FLAGS_DE_HIDL is removed. */ @MainThread -public class BiometricScheduler { +public class BiometricScheduler<T, U> { - private static final String BASE_TAG = "BiometricScheduler"; + private static final String TAG = "BiometricScheduler"; // Number of recent operations to keep in our logs for dumpsys protected static final int LOG_NUM_RECENT_OPERATIONS = 50; @@ -89,30 +99,6 @@ public class BiometricScheduler { @Retention(RetentionPolicy.SOURCE) public @interface SensorType {} - public static @SensorType int sensorTypeFromFingerprintProperties( - @NonNull FingerprintSensorPropertiesInternal props) { - if (props.isAnyUdfpsType()) { - return SENSOR_TYPE_UDFPS; - } - - return SENSOR_TYPE_FP_OTHER; - } - - public static String sensorTypeToString(@SensorType int sensorType) { - switch (sensorType) { - case SENSOR_TYPE_UNKNOWN: - return "Unknown"; - case SENSOR_TYPE_FACE: - return "Face"; - case SENSOR_TYPE_UDFPS: - return "Udfps"; - case SENSOR_TYPE_FP_OTHER: - return "OtherFp"; - default: - return "UnknownUnknown"; - } - } - private static final class CrashState { static final int NUM_ENTRIES = 10; final String timestamp; @@ -145,8 +131,8 @@ public class BiometricScheduler { } } - @NonNull protected final String mBiometricTag; - private final @SensorType int mSensorType; + @SensorType + private final int mSensorType; @Nullable private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher; @NonNull private final IBiometricService mBiometricService; @NonNull protected final Handler mHandler; @@ -157,6 +143,43 @@ public class BiometricScheduler { private int mTotalOperationsHandled; private final int mRecentOperationsLimit; @NonNull private final List<Integer> mRecentOperations; + @Nullable private StopUserClient<U> mStopUserClient; + @NonNull private Supplier<Integer> mCurrentUserRetriever; + @Nullable private UserSwitchProvider<T, U> mUserSwitchProvider; + + private class UserSwitchClientCallback implements ClientMonitorCallback { + @NonNull private final BaseClientMonitor mOwner; + + UserSwitchClientCallback(@NonNull BaseClientMonitor owner) { + mOwner = owner; + } + + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { + mHandler.post(() -> { + Slog.d(TAG, "[Client finished] " + clientMonitor + ", success: " + success); + + // Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible + // for that the queue will wait indefinitely until the field is cleared. + if (clientMonitor instanceof StopUserClient<?>) { + if (!success) { + Slog.w(TAG, "StopUserClient failed(), is the HAL stuck? " + + "Clearing mStopUserClient"); + } + mStopUserClient = null; + } + if (mCurrentOperation != null && mCurrentOperation.isFor(mOwner)) { + mCurrentOperation = null; + } else { + // can happen if the hal dies and is usually okay + // do not unset the current operation that may be newer + Slog.w(TAG, "operation is already null or different (reset?): " + + mCurrentOperation); + } + startNextOperationIfIdle(); + }); + } + } // Internal callback, notified when an operation is complete. Notifies the requester // that the operation is complete, before performing internal scheduler work (such as @@ -164,26 +187,26 @@ public class BiometricScheduler { private final ClientMonitorCallback mInternalCallback = new ClientMonitorCallback() { @Override public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { - Slog.d(getTag(), "[Started] " + clientMonitor); + Slog.d(TAG, "[Started] " + clientMonitor); } @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { mHandler.post(() -> { if (mCurrentOperation == null) { - Slog.e(getTag(), "[Finishing] " + clientMonitor + Slog.e(TAG, "[Finishing] " + clientMonitor + " but current operation is null, success: " + success + ", possible lifecycle bug in clientMonitor implementation?"); return; } if (!mCurrentOperation.isFor(clientMonitor)) { - Slog.e(getTag(), "[Ignoring Finish] " + clientMonitor + " does not match" + Slog.e(TAG, "[Ignoring Finish] " + clientMonitor + " does not match" + " current: " + mCurrentOperation); return; } - Slog.d(getTag(), "[Finishing] " + clientMonitor + ", success: " + success); + Slog.d(TAG, "[Finishing] " + clientMonitor + ", success: " + success); if (mGestureAvailabilityDispatcher != null) { mGestureAvailabilityDispatcher.markSensorActive( @@ -202,13 +225,11 @@ public class BiometricScheduler { }; @VisibleForTesting - public BiometricScheduler(@NonNull String tag, - @NonNull Handler handler, + public BiometricScheduler(@NonNull Handler handler, @SensorType int sensorType, @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull IBiometricService biometricService, int recentOperationsLimit) { - mBiometricTag = tag; mHandler = handler; mSensorType = sensorType; mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher; @@ -219,49 +240,140 @@ public class BiometricScheduler { mRecentOperations = new ArrayList<>(); } + @VisibleForTesting + public BiometricScheduler(@NonNull Handler handler, + @SensorType int sensorType, + @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull IBiometricService biometricService, + int recentOperationsLimit, + @NonNull Supplier<Integer> currentUserRetriever, + @Nullable UserSwitchProvider<T, U> userSwitchProvider) { + mHandler = handler; + mSensorType = sensorType; + mGestureAvailabilityDispatcher = gestureAvailabilityDispatcher; + mPendingOperations = new ArrayDeque<>(); + mBiometricService = biometricService; + mCrashStates = new ArrayDeque<>(); + mRecentOperationsLimit = recentOperationsLimit; + mRecentOperations = new ArrayList<>(); + mCurrentUserRetriever = currentUserRetriever; + mUserSwitchProvider = userSwitchProvider; + } + + public BiometricScheduler(@NonNull Handler handler, + @SensorType int sensorType, + @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull Supplier<Integer> currentUserRetriever, + @NonNull UserSwitchProvider<T, U> userSwitchProvider) { + this(handler, sensorType, gestureAvailabilityDispatcher, + IBiometricService.Stub.asInterface(ServiceManager.getService( + Context.BIOMETRIC_SERVICE)), LOG_NUM_RECENT_OPERATIONS, + currentUserRetriever, userSwitchProvider); + } + /** * Creates a new scheduler. * - * @param tag for the specific instance of the scheduler. Should be unique. * @param sensorType the sensorType that this scheduler is handling. * @param gestureAvailabilityDispatcher may be null if the sensor does not support gestures * (such as fingerprint swipe). */ - public BiometricScheduler(@NonNull String tag, - @SensorType int sensorType, + public BiometricScheduler(@SensorType int sensorType, @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { - this(tag, new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher, + this(new Handler(Looper.getMainLooper()), sensorType, gestureAvailabilityDispatcher, IBiometricService.Stub.asInterface( ServiceManager.getService(Context.BIOMETRIC_SERVICE)), LOG_NUM_RECENT_OPERATIONS); } + /** + * Returns sensor type for a fingerprint sensor. + */ + @SensorType + public static int sensorTypeFromFingerprintProperties( + @NonNull FingerprintSensorPropertiesInternal props) { + if (props.isAnyUdfpsType()) { + return SENSOR_TYPE_UDFPS; + } + + return SENSOR_TYPE_FP_OTHER; + } + @VisibleForTesting public ClientMonitorCallback getInternalCallback() { return mInternalCallback; } - protected String getTag() { - return BASE_TAG + "/" + mBiometricTag; + protected void startNextOperationIfIdle() { + if (Flags.deHidl()) { + startNextOperation(); + } else { + startNextOperationIfIdleLegacy(); + } + } + + protected void startNextOperation() { + if (mCurrentOperation != null) { + Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation); + return; + } + if (mPendingOperations.isEmpty()) { + Slog.d(TAG, "No operations, returning to idle"); + return; + } + + final int currentUserId = mCurrentUserRetriever.get(); + final int nextUserId = mPendingOperations.getFirst().getTargetUserId(); + + if (nextUserId == currentUserId || mPendingOperations.getFirst().isStartUserOperation()) { + startNextOperationIfIdleLegacy(); + } else if (currentUserId == UserHandle.USER_NULL && mUserSwitchProvider != null) { + final BaseClientMonitor startClient = + mUserSwitchProvider.getStartUserClient(nextUserId); + final UserSwitchClientCallback finishedCallback = + new UserSwitchClientCallback(startClient); + + Slog.d(TAG, "[Starting User] " + startClient); + mCurrentOperation = new BiometricSchedulerOperation( + startClient, finishedCallback, STATE_STARTED); + startClient.start(finishedCallback); + } else if (mUserSwitchProvider != null) { + if (mStopUserClient != null) { + Slog.d(TAG, "[Waiting for StopUser] " + mStopUserClient); + } else { + mStopUserClient = mUserSwitchProvider + .getStopUserClient(currentUserId); + final UserSwitchClientCallback finishedCallback = + new UserSwitchClientCallback(mStopUserClient); + + Slog.d(TAG, "[Stopping User] current: " + currentUserId + + ", next: " + nextUserId + ". " + mStopUserClient); + mCurrentOperation = new BiometricSchedulerOperation( + mStopUserClient, finishedCallback, STATE_STARTED); + mStopUserClient.start(finishedCallback); + } + } else { + Slog.e(TAG, "Cannot start next operation."); + } } - protected void startNextOperationIfIdle() { + protected void startNextOperationIfIdleLegacy() { if (mCurrentOperation != null) { - Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation); + Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation); return; } if (mPendingOperations.isEmpty()) { - Slog.d(getTag(), "No operations, returning to idle"); + Slog.d(TAG, "No operations, returning to idle"); return; } mCurrentOperation = mPendingOperations.poll(); - Slog.d(getTag(), "[Polled] " + mCurrentOperation); + Slog.d(TAG, "[Polled] " + mCurrentOperation); // If the operation at the front of the queue has been marked for cancellation, send // ERROR_CANCELED. No need to start this client. if (mCurrentOperation.isMarkedCanceling()) { - Slog.d(getTag(), "[Now Cancelling] " + mCurrentOperation); + Slog.d(TAG, "[Now Cancelling] " + mCurrentOperation); mCurrentOperation.cancel(mHandler, mInternalCallback); // Now we wait for the client to send its FinishCallback, which kicks off the next // operation. @@ -289,7 +401,7 @@ public class BiometricScheduler { // Note down current length of queue final int pendingOperationsLength = mPendingOperations.size(); final BiometricSchedulerOperation lastOperation = mPendingOperations.peekLast(); - Slog.e(getTag(), "[Unable To Start] " + mCurrentOperation + Slog.e(TAG, "[Unable To Start] " + mCurrentOperation + ". Last pending operation: " + lastOperation); // Then for each operation currently in the pending queue at the time of this @@ -298,10 +410,10 @@ public class BiometricScheduler { for (int i = 0; i < pendingOperationsLength; i++) { final BiometricSchedulerOperation operation = mPendingOperations.pollFirst(); if (operation != null) { - Slog.w(getTag(), "[Aborting Operation] " + operation); + Slog.w(TAG, "[Aborting Operation] " + operation); operation.abort(); } else { - Slog.e(getTag(), "Null operation, index: " + i + Slog.e(TAG, "Null operation, index: " + i + ", expected length: " + pendingOperationsLength); } } @@ -317,9 +429,9 @@ public class BiometricScheduler { mBiometricService.onReadyForAuthentication( mCurrentOperation.getClientMonitor().getRequestId(), cookie); } catch (RemoteException e) { - Slog.e(getTag(), "Remote exception when contacting BiometricService", e); + Slog.e(TAG, "Remote exception when contacting BiometricService", e); } - Slog.d(getTag(), "Waiting for cookie before starting: " + mCurrentOperation); + Slog.d(TAG, "Waiting for cookie before starting: " + mCurrentOperation); } } @@ -338,14 +450,14 @@ public class BiometricScheduler { */ public void startPreparedClient(int cookie) { if (mCurrentOperation == null) { - Slog.e(getTag(), "Current operation is null"); + Slog.e(TAG, "Current operation is null"); return; } if (mCurrentOperation.startWithCookie(mInternalCallback, cookie)) { - Slog.d(getTag(), "[Started] Prepared client: " + mCurrentOperation); + Slog.d(TAG, "[Started] Prepared client: " + mCurrentOperation); } else { - Slog.e(getTag(), "[Unable To Start] Prepared client: " + mCurrentOperation); + Slog.e(TAG, "[Unable To Start] Prepared client: " + mCurrentOperation); mCurrentOperation = null; startNextOperationIfIdle(); } @@ -374,13 +486,13 @@ public class BiometricScheduler { if (clientMonitor.interruptsPrecedingClients()) { for (BiometricSchedulerOperation operation : mPendingOperations) { if (operation.markCanceling()) { - Slog.d(getTag(), "New client, marking pending op as canceling: " + operation); + Slog.d(TAG, "New client, marking pending op as canceling: " + operation); } } } mPendingOperations.add(new BiometricSchedulerOperation(clientMonitor, clientCallback)); - Slog.d(getTag(), "[Added] " + clientMonitor + Slog.d(TAG, "[Added] " + clientMonitor + ", new queue size: " + mPendingOperations.size()); // If the new operation should interrupt preceding clients, and if the current operation is @@ -389,7 +501,7 @@ public class BiometricScheduler { && mCurrentOperation != null && mCurrentOperation.isInterruptable() && mCurrentOperation.isStarted()) { - Slog.d(getTag(), "[Cancelling Interruptable]: " + mCurrentOperation); + Slog.d(TAG, "[Cancelling Interruptable]: " + mCurrentOperation); mCurrentOperation.cancel(mHandler, mInternalCallback); } else { startNextOperationIfIdle(); @@ -401,16 +513,16 @@ public class BiometricScheduler { * @param token from the caller, should match the token passed in when requesting enrollment */ public void cancelEnrollment(IBinder token, long requestId) { - Slog.d(getTag(), "cancelEnrollment, requestId: " + requestId); + Slog.d(TAG, "cancelEnrollment, requestId: " + requestId); if (mCurrentOperation != null && canCancelEnrollOperation(mCurrentOperation, token, requestId)) { - Slog.d(getTag(), "Cancelling enrollment op: " + mCurrentOperation); + Slog.d(TAG, "Cancelling enrollment op: " + mCurrentOperation); mCurrentOperation.cancel(mHandler, mInternalCallback); } else { for (BiometricSchedulerOperation operation : mPendingOperations) { if (canCancelEnrollOperation(operation, token, requestId)) { - Slog.d(getTag(), "Cancelling pending enrollment op: " + operation); + Slog.d(TAG, "Cancelling pending enrollment op: " + operation); operation.markCanceling(); } } @@ -423,16 +535,16 @@ public class BiometricScheduler { * @param requestId the id returned when requesting authentication */ public void cancelAuthenticationOrDetection(IBinder token, long requestId) { - Slog.d(getTag(), "cancelAuthenticationOrDetection, requestId: " + requestId); + Slog.d(TAG, "cancelAuthenticationOrDetection, requestId: " + requestId); if (mCurrentOperation != null && canCancelAuthOperation(mCurrentOperation, token, requestId)) { - Slog.d(getTag(), "Cancelling auth/detect op: " + mCurrentOperation); + Slog.d(TAG, "Cancelling auth/detect op: " + mCurrentOperation); mCurrentOperation.cancel(mHandler, mInternalCallback); } else { for (BiometricSchedulerOperation operation : mPendingOperations) { if (canCancelAuthOperation(operation, token, requestId)) { - Slog.d(getTag(), "Cancelling pending auth/detect op: " + operation); + Slog.d(TAG, "Cancelling pending auth/detect op: " + operation); operation.markCanceling(); } } @@ -504,11 +616,11 @@ public class BiometricScheduler { mCurrentOperation != null ? mCurrentOperation.toString() : null, pendingOperations); mCrashStates.add(crashState); - Slog.e(getTag(), "Recorded crash state: " + crashState.toString()); + Slog.e(TAG, "Recorded crash state: " + crashState.toString()); } public void dump(PrintWriter pw) { - pw.println("Dump of BiometricScheduler " + getTag()); + pw.println("Dump of BiometricScheduler " + TAG); pw.println("Type: " + mSensorType); pw.println("Current operation: " + mCurrentOperation); pw.println("Pending operations: " + mPendingOperations.size()); @@ -548,7 +660,7 @@ public class BiometricScheduler { * HAL dies. */ public void reset() { - Slog.d(getTag(), "Resetting scheduler"); + Slog.d(TAG, "Resetting scheduler"); mPendingOperations.clear(); mCurrentOperation = null; } @@ -562,11 +674,11 @@ public class BiometricScheduler { return; } for (BiometricSchedulerOperation pendingOperation : mPendingOperations) { - Slog.d(getTag(), "[Watchdog cancelling pending] " + Slog.d(TAG, "[Watchdog cancelling pending] " + pendingOperation.getClientMonitor()); pendingOperation.markCancelingForWatchdog(); } - Slog.d(getTag(), "[Watchdog cancelling current] " + Slog.d(TAG, "[Watchdog cancelling current] " + mCurrentOperation.getClientMonitor()); mCurrentOperation.cancel(mHandler, getInternalCallback()); } @@ -590,9 +702,23 @@ public class BiometricScheduler { /** * Handle stop user client when user switching occurs. */ - public void onUserStopped() {} + public void onUserStopped() { + if (mStopUserClient == null) { + Slog.e(TAG, "Unexpected onUserStopped"); + return; + } + + Slog.d(TAG, "[OnUserStopped]: " + mStopUserClient); + mStopUserClient.onUserStopped(); + mStopUserClient = null; + } public Handler getHandler() { return mHandler; } + + @Nullable + public StopUserClient<?> getStopUserClient() { + return mStopUserClient; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java index e8654dc059a4..e01c4ec76ed2 100644 --- a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java @@ -30,7 +30,10 @@ import java.util.function.Supplier; /** * Abstract class for stopping a user. - * @param <T> Interface for stopping the user. + * + * @param <T> Session for stopping the user. It should be either an instance of + * {@link com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession} or + * {@link com.android.server.biometrics.sensors.face.aidl.AidlSession}. */ public abstract class StopUserClient<T> extends HalClientMonitor<T> { diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java index 3753bbdba276..7ca10e3f9c98 100644 --- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java @@ -33,10 +33,14 @@ import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDisp /** * A user-aware scheduler that requests user-switches based on scheduled operation's targetUserId. + * TODO (b/304604965): Remove class when Flags.FLAG_DE_HIDL is removed. + * + * @param <T> Hal instance for starting the user. + * @param <U> Session associated with the current user id. */ -public class UserAwareBiometricScheduler extends BiometricScheduler { +public class UserAwareBiometricScheduler<T, U> extends BiometricScheduler<T, U> { - private static final String BASE_TAG = "UaBiometricScheduler"; + private static final String TAG = "UaBiometricScheduler"; /** * Interface to retrieve the owner's notion of the current userId. Note that even though @@ -66,13 +70,13 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { mHandler.post(() -> { - Slog.d(getTag(), "[Client finished] " + clientMonitor + ", success: " + success); + Slog.d(TAG, "[Client finished] " + clientMonitor + ", success: " + success); // Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible // for that the queue will wait indefinitely until the field is cleared. if (clientMonitor instanceof StopUserClient<?>) { if (!success) { - Slog.w(getTag(), "StopUserClient failed(), is the HAL stuck? " + Slog.w(TAG, "StopUserClient failed(), is the HAL stuck? " + "Clearing mStopUserClient"); } mStopUserClient = null; @@ -82,7 +86,7 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { } else { // can happen if the hal dies and is usually okay // do not unset the current operation that may be newer - Slog.w(getTag(), "operation is already null or different (reset?): " + Slog.w(TAG, "operation is already null or different (reset?): " + mCurrentOperation); } startNextOperationIfIdle(); @@ -98,7 +102,7 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { @NonNull IBiometricService biometricService, @NonNull CurrentUserRetriever currentUserRetriever, @NonNull UserSwitchCallback userSwitchCallback) { - super(tag, handler, sensorType, gestureAvailabilityDispatcher, biometricService, + super(handler, sensorType, gestureAvailabilityDispatcher, biometricService, LOG_NUM_RECENT_OPERATIONS); mCurrentUserRetriever = currentUserRetriever; @@ -117,18 +121,13 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { } @Override - protected String getTag() { - return BASE_TAG + "/" + mBiometricTag; - } - - @Override protected void startNextOperationIfIdle() { if (mCurrentOperation != null) { - Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation); + Slog.v(TAG, "Not idle, current operation: " + mCurrentOperation); return; } if (mPendingOperations.isEmpty()) { - Slog.d(getTag(), "No operations, returning to idle"); + Slog.d(TAG, "No operations, returning to idle"); return; } @@ -143,20 +142,20 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { final ClientFinishedCallback finishedCallback = new ClientFinishedCallback(startClient); - Slog.d(getTag(), "[Starting User] " + startClient); + Slog.d(TAG, "[Starting User] " + startClient); mCurrentOperation = new BiometricSchedulerOperation( startClient, finishedCallback, STATE_STARTED); startClient.start(finishedCallback); } else { if (mStopUserClient != null) { - Slog.d(getTag(), "[Waiting for StopUser] " + mStopUserClient); + Slog.d(TAG, "[Waiting for StopUser] " + mStopUserClient); } else { mStopUserClient = mUserSwitchCallback .getStopUserClient(currentUserId); final ClientFinishedCallback finishedCallback = new ClientFinishedCallback(mStopUserClient); - Slog.d(getTag(), "[Stopping User] current: " + currentUserId + Slog.d(TAG, "[Stopping User] current: " + currentUserId + ", next: " + nextUserId + ". " + mStopUserClient); mCurrentOperation = new BiometricSchedulerOperation( mStopUserClient, finishedCallback, STATE_STARTED); @@ -168,11 +167,11 @@ public class UserAwareBiometricScheduler extends BiometricScheduler { @Override public void onUserStopped() { if (mStopUserClient == null) { - Slog.e(getTag(), "Unexpected onUserStopped"); + Slog.e(TAG, "Unexpected onUserStopped"); return; } - Slog.d(getTag(), "[OnUserStopped]: " + mStopUserClient); + Slog.d(TAG, "[OnUserStopped]: " + mStopUserClient); mStopUserClient.onUserStopped(); mStopUserClient = null; } diff --git a/services/core/java/com/android/server/biometrics/sensors/UserSwitchProvider.java b/services/core/java/com/android/server/biometrics/sensors/UserSwitchProvider.java new file mode 100644 index 000000000000..bc5c55b99ab3 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/UserSwitchProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.annotation.NonNull; + +/** + * Interface to get the appropriate start and stop user clients. + * + * @param <T> Hal instance for starting the user. + * @param <U> Session associated with the current user id. + */ +public interface UserSwitchProvider<T, U> { + @NonNull + StartUserClient<T, U> getStartUserClient(int newUserId); + @NonNull + StopUserClient<U> getStopUserClient(int userId); +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java index af46f441d6ce..3d61f993b7d2 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlSession.java @@ -53,12 +53,10 @@ public class AidlSession { mAidlResponseHandler = aidlResponseHandler; } - /** The underlying {@link ISession}. */ @NonNull public ISession getSession() { return mSession; } - /** The user id associated with the session. */ public int getUserId() { return mUserId; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index 9fa15b8ea3a1..e4ecf1a61155 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -39,6 +39,7 @@ import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceServiceReceiver; import android.os.Binder; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; @@ -88,6 +89,8 @@ import java.util.concurrent.atomic.AtomicLong; * Provider for a single instance of the {@link IFace} HAL. */ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { + + private static final String TAG = "FaceProvider"; private static final int ENROLL_TIMEOUT_SEC = 75; private boolean mTestHalEnabled; @@ -159,7 +162,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull BiometricContext biometricContext, boolean resetLockoutRequiresChallenge) { this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher, - biometricContext, null /* daemon */, resetLockoutRequiresChallenge, + biometricContext, null /* daemon */, getHandler(), resetLockoutRequiresChallenge, false /* testHalEnabled */); } @@ -169,13 +172,19 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext, - @Nullable IFace daemon, boolean resetLockoutRequiresChallenge, + @Nullable IFace daemon, + @NonNull Handler handler, + boolean resetLockoutRequiresChallenge, boolean testHalEnabled) { mContext = context; mBiometricStateCallback = biometricStateCallback; mHalInstanceName = halInstanceName; mFaceSensors = new SensorList<>(ActivityManager.getService()); - mHandler = new Handler(Looper.getMainLooper()); + if (Flags.deHidl()) { + mHandler = handler; + } else { + mHandler = new Handler(Looper.getMainLooper()); + } mUsageStats = new UsageStats(context); mLockoutResetDispatcher = lockoutResetDispatcher; mActivityTaskManager = ActivityTaskManager.getInstance(); @@ -189,6 +198,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { initSensors(resetLockoutRequiresChallenge, props); } + @NonNull + private static Handler getHandler() { + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + return new Handler(handlerThread.getLooper()); + } + private void initAuthenticationBroadcastReceiver() { new AuthenticationStatsBroadcastReceiver( mContext, @@ -230,8 +246,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType, prop.supportsDetectInteraction, prop.halControlsPreview, false /* resetLockoutRequiresChallenge */); - final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, - mContext, mHandler, internalProp, mLockoutResetDispatcher, + final Sensor sensor = new Sensor(this, + mContext, mHandler, internalProp, mBiometricContext); sensor.init(mLockoutResetDispatcher, this); final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : @@ -250,9 +266,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private void addHidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) { final int sensorId = prop.commonProps.sensorId; - final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/" + sensorId, this, - mContext, mHandler, prop, mLockoutResetDispatcher, - mBiometricContext, resetLockoutRequiresChallenge, + final Sensor sensor = new HidlToAidlSensorAdapter(this, mContext, mHandler, prop, + mLockoutResetDispatcher, mBiometricContext, resetLockoutRequiresChallenge, () -> { //TODO: update to make this testable scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(), @@ -279,8 +294,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private void addAidlSensors(SensorProps prop, boolean resetLockoutRequiresChallenge) { final int sensorId = prop.commonProps.sensorId; - final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, - mHandler, prop, mLockoutResetDispatcher, mBiometricContext, + final Sensor sensor = new Sensor(this, mContext, mHandler, prop, mBiometricContext, resetLockoutRequiresChallenge); sensor.init(mLockoutResetDispatcher, this); final int userId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : @@ -296,7 +310,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } private String getTag() { - return "FaceProvider/" + mHalInstanceName; + return TAG + "/" + mHalInstanceName; } boolean hasHalInstance() { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java index 0110ae991ae4..e5ae8e336dcb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStopUserClient.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.face.aidl; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.biometrics.face.ISession; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -30,10 +31,10 @@ import com.android.server.biometrics.sensors.StopUserClient; import java.util.function.Supplier; -public class FaceStopUserClient extends StopUserClient<AidlSession> { +public class FaceStopUserClient extends StopUserClient<ISession> { private static final String TAG = "FaceStopUserClient"; - public FaceStopUserClient(@NonNull Context context, @NonNull Supplier<AidlSession> lazyDaemon, + public FaceStopUserClient(@NonNull Context context, @NonNull Supplier<ISession> lazyDaemon, @Nullable IBinder token, int userId, int sensorId, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, @NonNull UserStoppedCallback callback) { @@ -49,7 +50,7 @@ public class FaceStopUserClient extends StopUserClient<AidlSession> { @Override protected void startHalOperation() { try { - getFreshDaemon().getSession().close(); + getFreshDaemon().close(); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); getCallback().onClientFinished(this, false /* success */); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java index 3e5c59914913..635e79a31a96 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -58,6 +58,7 @@ import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.StartUserClient; import com.android.server.biometrics.sensors.StopUserClient; import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.UserSwitchProvider; import com.android.server.biometrics.sensors.face.FaceUtils; import java.util.ArrayList; @@ -71,15 +72,16 @@ import java.util.function.Supplier; */ public class Sensor { + private static final String TAG = "Sensor"; + private boolean mTestHalEnabled; - @NonNull private final String mTag; @NonNull private final FaceProvider mProvider; @NonNull private final Context mContext; @NonNull private final IBinder mToken; @NonNull private final Handler mHandler; @NonNull private final FaceSensorPropertiesInternal mSensorProperties; - @NonNull private BiometricScheduler mScheduler; + @NonNull private BiometricScheduler<IFace, ISession> mScheduler; @Nullable private LockoutTracker mLockoutTracker; @NonNull private final Map<Integer, Long> mAuthenticatorIds; @@ -88,11 +90,9 @@ public class Sensor { @NonNull BiometricContext mBiometricContext; - Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, + Sensor(@NonNull FaceProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull BiometricContext biometricContext, @Nullable AidlSession session) { - mTag = tag; + @NonNull BiometricContext biometricContext) { mProvider = provider; mContext = context; mToken = new Binder(); @@ -102,105 +102,135 @@ public class Sensor { mAuthenticatorIds = new HashMap<>(); } - Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, - @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull BiometricContext biometricContext) { - this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher, - biometricContext, null); - } - - public Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context, + public Sensor(@NonNull FaceProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull SensorProps prop, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext, boolean resetLockoutRequiresChallenge) { - this(tag, provider, context, handler, + this(provider, context, handler, getFaceSensorPropertiesInternal(prop, resetLockoutRequiresChallenge), - lockoutResetDispatcher, biometricContext, null); + biometricContext); } /** * Initialize biometric scheduler, lockout tracker and session for the sensor. */ - public void init(LockoutResetDispatcher lockoutResetDispatcher, - FaceProvider provider) { - mScheduler = new UserAwareBiometricScheduler(mTag, - BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityDispatcher */, + public void init(@NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull FaceProvider provider) { + if (Flags.deHidl()) { + setScheduler(getBiometricSchedulerForInit(lockoutResetDispatcher, provider)); + } else { + setScheduler(getUserAwareBiometricSchedulerForInit(lockoutResetDispatcher, provider)); + } + mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; + mLockoutTracker = new LockoutCache(); + } + + private BiometricScheduler<IFace, ISession> getBiometricSchedulerForInit( + @NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull FaceProvider provider) { + return new BiometricScheduler<>(mHandler, + BiometricScheduler.SENSOR_TYPE_FACE, + null /* gestureAvailabilityDispatcher */, () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL, - new UserAwareBiometricScheduler.UserSwitchCallback() { + new UserSwitchProvider<IFace, ISession>() { @NonNull @Override - public StopUserClient<?> getStopUserClient(int userId) { - return new FaceStopUserClient(mContext, mLazySession, mToken, userId, - mSensorProperties.sensorId, - BiometricLogger.ofUnknown(mContext), mBiometricContext, - () -> mCurrentSession = null); + public StopUserClient<ISession> getStopUserClient(int userId) { + return new FaceStopUserClient(mContext, + () -> mLazySession.get().getSession(), mToken, userId, + mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext), + mBiometricContext, () -> mCurrentSession = null); } @NonNull @Override - public StartUserClient<?, ?> getStartUserClient(int newUserId) { + public StartUserClient<IFace, ISession> getStartUserClient(int newUserId) { final int sensorId = mSensorProperties.sensorId; + final AidlResponseHandler resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> { + }, + new AidlResponseHandler.AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() { + mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId, + newUserId); + mProvider.scheduleInvalidationRequest(sensorId, + newUserId); + } - final AidlResponseHandler resultController; - if (Flags.deHidl()) { - resultController = new AidlResponseHandler( - mContext, mScheduler, sensorId, newUserId, - mLockoutTracker, lockoutResetDispatcher, - mBiometricContext.getAuthSessionCoordinator(), () -> {}, - new AidlResponseHandler.AidlResponseHandlerCallback() { - @Override - public void onEnrollSuccess() { - mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId, - newUserId); - mProvider.scheduleInvalidationRequest(sensorId, - newUserId); - } - - @Override - public void onHardwareUnavailable() { - Slog.e(mTag, "Face sensor hardware unavailable."); - mCurrentSession = null; - } - }); - } else { - resultController = new AidlResponseHandler( - mContext, mScheduler, sensorId, newUserId, - mLockoutTracker, lockoutResetDispatcher, - mBiometricContext.getAuthSessionCoordinator(), () -> { - Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); - mCurrentSession = null; - }); - } - - final StartUserClient.UserStartedCallback<ISession> userStartedCallback = - (userIdStarted, newSession, halInterfaceVersion) -> { - Slog.d(mTag, "New session created for user: " - + userIdStarted + " with hal version: " - + halInterfaceVersion); - mCurrentSession = new AidlSession(halInterfaceVersion, - newSession, userIdStarted, resultController); - if (FaceUtils.getLegacyInstance(sensorId) - .isInvalidationInProgress(mContext, userIdStarted)) { - Slog.w(mTag, - "Scheduling unfinished invalidation request for " - + "sensor: " - + sensorId - + ", user: " + userIdStarted); - provider.scheduleInvalidationRequest(sensorId, - userIdStarted); + @Override + public void onHardwareUnavailable() { + Slog.e(TAG, "Face sensor hardware unavailable."); + mCurrentSession = null; } - }; + }); - return new FaceStartUserClient(mContext, provider::getHalInstance, - mToken, newUserId, mSensorProperties.sensorId, - BiometricLogger.ofUnknown(mContext), mBiometricContext, - resultController, userStartedCallback); + return Sensor.this.getStartUserClient(resultController, sensorId, + newUserId, provider); } }); - mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; - mLockoutTracker = new LockoutCache(); + } + + private UserAwareBiometricScheduler<IFace, ISession> getUserAwareBiometricSchedulerForInit( + LockoutResetDispatcher lockoutResetDispatcher, + FaceProvider provider) { + return new UserAwareBiometricScheduler<>(TAG, + BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityDispatcher */, + () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL, + new UserAwareBiometricScheduler.UserSwitchCallback() { + @NonNull + @Override + public StopUserClient<ISession> getStopUserClient(int userId) { + return new FaceStopUserClient(mContext, + () -> mLazySession.get().getSession(), mToken, userId, + mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext), + mBiometricContext, () -> mCurrentSession = null); + } + + @NonNull + @Override + public StartUserClient<IFace, ISession> getStartUserClient(int newUserId) { + final int sensorId = mSensorProperties.sensorId; + final AidlResponseHandler resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> { + Slog.e(TAG, "Face sensor hardware unavailable."); + mCurrentSession = null; + }); + + return Sensor.this.getStartUserClient(resultController, sensorId, + newUserId, provider); + } + }); + } + + private FaceStartUserClient getStartUserClient(@NonNull AidlResponseHandler resultController, + int sensorId, int newUserId, @NonNull FaceProvider provider) { + final StartUserClient.UserStartedCallback<ISession> userStartedCallback = + (userIdStarted, newSession, halInterfaceVersion) -> { + Slog.d(TAG, "New face session created for user: " + + userIdStarted + " with hal version: " + + halInterfaceVersion); + mCurrentSession = new AidlSession(halInterfaceVersion, + newSession, userIdStarted, resultController); + if (FaceUtils.getLegacyInstance(sensorId) + .isInvalidationInProgress(mContext, userIdStarted)) { + Slog.w(TAG, + "Scheduling unfinished invalidation request for " + + "face sensor: " + + sensorId + + ", user: " + userIdStarted); + provider.scheduleInvalidationRequest(sensorId, + userIdStarted); + } + }; + + return new FaceStartUserClient(mContext, provider::getHalInstance, mToken, newUserId, + mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext, + resultController, userStartedCallback); } private static FaceSensorPropertiesInternal getFaceSensorPropertiesInternal(SensorProps prop, @@ -213,13 +243,11 @@ public class Sensor { info.softwareVersion)); } } - final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( + return new FaceSensorPropertiesInternal( prop.commonProps.sensorId, prop.commonProps.sensorStrength, prop.commonProps.maxEnrollmentsPerUser, componentInfo, prop.sensorType, prop.supportsDetectInteraction, prop.halControlsPreview, resetLockoutRequiresChallenge); - - return internalProp; } @NonNull public Supplier<AidlSession> getLazySession() { @@ -243,7 +271,7 @@ public class Sensor { mProvider, this); } - @NonNull public BiometricScheduler getScheduler() { + @NonNull public BiometricScheduler<IFace, ISession> getScheduler() { return mScheduler; } @@ -259,17 +287,17 @@ public class Sensor { } void setTestHalEnabled(boolean enabled) { - Slog.w(mTag, "setTestHalEnabled: " + enabled); + Slog.w(TAG, "Face setTestHalEnabled: " + enabled); if (enabled != mTestHalEnabled) { // The framework should retrieve a new session from the HAL. try { if (mCurrentSession != null) { // TODO(181984005): This should be scheduled instead of directly invoked - Slog.d(mTag, "Closing old session"); + Slog.d(TAG, "Closing old face session"); mCurrentSession.getSession().close(); } } catch (RemoteException e) { - Slog.e(mTag, "RemoteException", e); + Slog.e(TAG, "RemoteException", e); } mCurrentSession = null; } @@ -308,7 +336,7 @@ public class Sensor { public void onBinderDied() { final BaseClientMonitor client = mScheduler.getCurrentClient(); if (client != null && client.isInterruptable()) { - Slog.e(mTag, "Sending ERROR_HW_UNAVAILABLE for client: " + client); + Slog.e(TAG, "Sending face hardware unavailable error for client: " + client); final ErrorConsumer errorConsumer = (ErrorConsumer) client; errorConsumer.onError(FaceManager.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index 46ce0b62e6d5..53376669b387 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -120,7 +120,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull private final FaceSensorPropertiesInternal mSensorProperties; @NonNull private final BiometricStateCallback mBiometricStateCallback; @NonNull private final Context mContext; - @NonNull private final BiometricScheduler mScheduler; + @NonNull private final BiometricScheduler<IBiometricsFace, AidlSession> mScheduler; @NonNull private final Handler mHandler; @NonNull private final Supplier<IBiometricsFace> mLazyDaemon; @NonNull private final LockoutHalImpl mLockoutTracker; @@ -163,14 +163,15 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { private final int mSensorId; @NonNull private final Context mContext; @NonNull private final Handler mHandler; - @NonNull private final BiometricScheduler mScheduler; + @NonNull private final BiometricScheduler<IBiometricsFace, AidlSession> mScheduler; @Nullable private Callback mCallback; @NonNull private final LockoutHalImpl mLockoutTracker; @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher; HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler, - @NonNull BiometricScheduler scheduler, @NonNull LockoutHalImpl lockoutTracker, + @NonNull BiometricScheduler<IBiometricsFace, AidlSession> scheduler, + @NonNull LockoutHalImpl lockoutTracker, @NonNull LockoutResetDispatcher lockoutResetDispatcher) { mSensorId = sensorId; mContext = context; @@ -352,7 +353,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull Handler handler, - @NonNull BiometricScheduler scheduler, + @NonNull BiometricScheduler<IBiometricsFace, AidlSession> scheduler, @NonNull BiometricContext biometricContext) { mSensorProperties = sensorProps; mContext = context; @@ -395,7 +396,8 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull LockoutResetDispatcher lockoutResetDispatcher) { final Handler handler = new Handler(Looper.getMainLooper()); return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher, - handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, + handler, new BiometricScheduler<>( + BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityTracker */), BiometricContext.getInstance(context)); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java index 6355cb57a752..a004cae4ae26 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.pm.UserInfo; +import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.SensorProps; import android.hardware.biometrics.face.V1_0.IBiometricsFace; import android.os.Handler; @@ -67,8 +68,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe }; private LockoutHalImpl mLockoutTracker; - public HidlToAidlSensorAdapter(@NonNull String tag, - @NonNull FaceProvider provider, + public HidlToAidlSensorAdapter(@NonNull FaceProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull SensorProps prop, @@ -76,15 +76,14 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe @NonNull BiometricContext biometricContext, boolean resetLockoutRequiresChallenge, @NonNull Runnable internalCleanupAndGetFeatureRunnable) { - this(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext, + this(provider, context, handler, prop, lockoutResetDispatcher, biometricContext, resetLockoutRequiresChallenge, internalCleanupAndGetFeatureRunnable, new AuthSessionCoordinator(), null /* daemon */, null /* onEnrollSuccessCallback */); } @VisibleForTesting - HidlToAidlSensorAdapter(@NonNull String tag, - @NonNull FaceProvider provider, + HidlToAidlSensorAdapter(@NonNull FaceProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull SensorProps prop, @@ -95,7 +94,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe @NonNull AuthSessionCoordinator authSessionCoordinator, @Nullable IBiometricsFace daemon, @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) { - super(tag, provider, context, handler, prop, lockoutResetDispatcher, biometricContext, + super(provider, context, handler, prop, biometricContext, resetLockoutRequiresChallenge); mInternalCleanupAndGetFeatureRunnable = internalCleanupAndGetFeatureRunnable; mFaceProvider = provider; @@ -124,7 +123,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe @Override public void serviceDied(long cookie) { - Slog.d(TAG, "HAL died."); + Slog.d(TAG, "Face HAL died."); mDaemon = null; } @@ -140,10 +139,12 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } @Override - public void init(LockoutResetDispatcher lockoutResetDispatcher, - FaceProvider provider) { - setScheduler(new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, - null /* gestureAvailabilityTracker */)); + public void init(@NonNull LockoutResetDispatcher lockoutResetDispatcher, + @NonNull FaceProvider provider) { + setScheduler(new BiometricScheduler<ISession, AidlSession>(getHandler(), + BiometricScheduler.SENSOR_TYPE_FACE, + null /* gestureAvailabilityTracker */, () -> mCurrentUserId, + null /* userSwitchProvider */)); setLazySession(this::getSession); mLockoutTracker = new LockoutHalImpl(); } @@ -188,7 +189,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe return mDaemon; } - Slog.d(TAG, "Daemon was null, reconnecting, current operation: " + Slog.d(TAG, "Face daemon was null, reconnecting, current operation: " + getScheduler().getCurrentClient()); try { @@ -213,7 +214,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } @VisibleForTesting void handleUserChanged(int newUserId) { - Slog.d(TAG, "User changed. Current user is " + newUserId); + Slog.d(TAG, "User changed. Current user for face sensor is " + newUserId); mSession = null; mCurrentUserId = newUserId; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java index 5daf2d4fbcf4..fa953615a1d6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSessionAdapter.java @@ -282,7 +282,7 @@ public class HidlToAidlSessionAdapter implements ISession { @Override public ICancellationSignal enrollWithOptions(FaceEnrollOptions options) { - //Unsupported in HIDL + Slog.e(TAG, "enrollWithOptions unsupported in HIDL"); return null; } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java index 8ff105baa981..0d4dac089907 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlSession.java @@ -51,12 +51,10 @@ public class AidlSession { mAidlResponseHandler = aidlResponseHandler; } - /** The underlying {@link ISession}. */ @NonNull public ISession getSession() { return mSession; } - /** The user id associated with the session. */ public int getUserId() { return mUserId; } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 88a11d9c0ceb..c0388d1c4f21 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -46,6 +46,7 @@ import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.Binder; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; @@ -102,6 +103,8 @@ import java.util.stream.Collectors; @SuppressWarnings("deprecation") public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvider { + private static final String TAG = "FingerprintProvider"; + private boolean mTestHalEnabled; @NonNull @@ -172,7 +175,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi boolean resetLockoutRequiresHardwareAuthToken) { this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName, lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, - null /* daemon */, resetLockoutRequiresHardwareAuthToken, + null /* daemon */, getHandler(), resetLockoutRequiresHardwareAuthToken, false /* testHalEnabled */); } @@ -184,6 +187,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, @Nullable IFingerprint daemon, + @NonNull Handler handler, boolean resetLockoutRequiresHardwareAuthToken, boolean testHalEnabled) { mContext = context; @@ -191,7 +195,11 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mAuthenticationStateListeners = authenticationStateListeners; mHalInstanceName = halInstanceName; mFingerprintSensors = new SensorList<>(ActivityManager.getService()); - mHandler = new Handler(Looper.getMainLooper()); + if (Flags.deHidl()) { + mHandler = handler; + } else { + mHandler = new Handler(Looper.getMainLooper()); + } mLockoutResetDispatcher = lockoutResetDispatcher; mActivityTaskManager = ActivityTaskManager.getInstance(); mTaskStackListener = new BiometricTaskStackListener(); @@ -204,6 +212,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher); } + @NonNull + private static Handler getHandler() { + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + return new Handler(handlerThread.getLooper()); + } + private void initAuthenticationBroadcastReceiver() { new AuthenticationStatsBroadcastReceiver( mContext, @@ -262,11 +277,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi location.sensorLocationY, location.sensorRadius)) .collect(Collectors.toList())); - final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, - mHandler, internalProp, mLockoutResetDispatcher, - gestureAvailabilityDispatcher, mBiometricContext); - sensor.init(gestureAvailabilityDispatcher, - mLockoutResetDispatcher); + final Sensor sensor = new Sensor(this, mContext, mHandler, internalProp, + mBiometricContext); + sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher); final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : sensor.getLazySession().get().getUserId(); @@ -286,10 +299,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, boolean resetLockoutRequiresHardwareAuthToken) { final int sensorId = prop.commonProps.sensorId; - final Sensor sensor = new HidlToAidlSensorAdapter(getTag() + "/" - + sensorId, this, mContext, mHandler, - prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher, - mBiometricContext, resetLockoutRequiresHardwareAuthToken, + final Sensor sensor = new HidlToAidlSensorAdapter(this, mContext, mHandler, prop, + mLockoutResetDispatcher, mBiometricContext, resetLockoutRequiresHardwareAuthToken, () -> scheduleInternalCleanup(sensorId, ActivityManager.getCurrentUser(), null /* callback */)); sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher); @@ -307,14 +318,11 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi private void addAidlSensors(@NonNull SensorProps prop, @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, - List<SensorLocationInternal> workaroundLocations, + @NonNull List<SensorLocationInternal> workaroundLocations, boolean resetLockoutRequiresHardwareAuthToken) { final int sensorId = prop.commonProps.sensorId; - final Sensor sensor = new Sensor(getTag() + "/" + sensorId, - this, mContext, mHandler, - prop, mLockoutResetDispatcher, gestureAvailabilityDispatcher, - mBiometricContext, workaroundLocations, - resetLockoutRequiresHardwareAuthToken); + final Sensor sensor = new Sensor(this, mContext, mHandler, prop, mBiometricContext, + workaroundLocations, resetLockoutRequiresHardwareAuthToken); sensor.init(gestureAvailabilityDispatcher, mLockoutResetDispatcher); final int sessionUserId = sensor.getLazySession().get() == null ? UserHandle.USER_NULL : sensor.getLazySession().get().getUserId(); @@ -329,7 +337,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } private String getTag() { - return "FingerprintProvider/" + mHalInstanceName; + return TAG + "/" + mHalInstanceName; } boolean hasHalInstance() { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java index 2cc1879c0851..394f04520531 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStopUserClient.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.biometrics.fingerprint.ISession; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -30,11 +31,11 @@ import com.android.server.biometrics.sensors.StopUserClient; import java.util.function.Supplier; -public class FingerprintStopUserClient extends StopUserClient<AidlSession> { +public class FingerprintStopUserClient extends StopUserClient<ISession> { private static final String TAG = "FingerprintStopUserClient"; public FingerprintStopUserClient(@NonNull Context context, - @NonNull Supplier<AidlSession> lazyDaemon, @Nullable IBinder token, int userId, + @NonNull Supplier<ISession> lazyDaemon, @Nullable IBinder token, int userId, int sensorId, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, @NonNull UserStoppedCallback callback) { @@ -50,7 +51,7 @@ public class FingerprintStopUserClient extends StopUserClient<AidlSession> { @Override protected void startHalOperation() { try { - getFreshDaemon().getSession().close(); + getFreshDaemon().close(); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); getCallback().onClientFinished(this, false /* success */); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java index dd887bb05c12..af88c62904fc 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java @@ -59,6 +59,7 @@ import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.StartUserClient; import com.android.server.biometrics.sensors.StopUserClient; import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.UserSwitchProvider; import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; @@ -77,15 +78,16 @@ import java.util.stream.Collectors; @SuppressWarnings("deprecation") public class Sensor { + private static final String TAG = "Sensor"; + private boolean mTestHalEnabled; - @NonNull private final String mTag; @NonNull private final FingerprintProvider mProvider; @NonNull private final Context mContext; @NonNull private final IBinder mToken; @NonNull private final Handler mHandler; @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties; - @NonNull private BiometricScheduler mScheduler; + @NonNull private BiometricScheduler<IFingerprint, ISession> mScheduler; @NonNull private LockoutTracker mLockoutTracker; @NonNull private final Map<Integer, Long> mAuthenticatorIds; @NonNull private final BiometricContext mBiometricContext; @@ -93,13 +95,10 @@ public class Sensor { @Nullable AidlSession mCurrentSession; @NonNull private Supplier<AidlSession> mLazySession; - public Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, + public Sensor(@NonNull FingerprintProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, AidlSession session) { - mTag = tag; mProvider = provider; mContext = context; mToken = new Binder(); @@ -110,41 +109,52 @@ public class Sensor { mCurrentSession = session; } - Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context, + Sensor(@NonNull FingerprintProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext) { - this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher, - gestureAvailabilityDispatcher, biometricContext, null); + this(provider, context, handler, sensorProperties, + biometricContext, null); } - Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context, + Sensor(@NonNull FingerprintProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull SensorProps sensorProp, - @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, @NonNull List<SensorLocationInternal> workaroundLocation, boolean resetLockoutRequiresHardwareAuthToken) { - this(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(sensorProp, + this(provider, context, handler, getFingerprintSensorPropertiesInternal(sensorProp, workaroundLocation, resetLockoutRequiresHardwareAuthToken), - lockoutResetDispatcher, gestureAvailabilityDispatcher, biometricContext, null); + biometricContext, null); } /** * Initialize biometric scheduler, lockout tracker and session for the sensor. */ - public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher, - LockoutResetDispatcher lockoutResetDispatcher) { - mScheduler = new UserAwareBiometricScheduler(mTag, + public void init(@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull LockoutResetDispatcher lockoutResetDispatcher) { + if (Flags.deHidl()) { + setScheduler(getBiometricSchedulerForInit(gestureAvailabilityDispatcher, + lockoutResetDispatcher)); + } else { + setScheduler(getUserAwareBiometricSchedulerForInit(gestureAvailabilityDispatcher, + lockoutResetDispatcher)); + } + mLockoutTracker = new LockoutCache(); + mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; + } + + private BiometricScheduler<IFingerprint, ISession> getBiometricSchedulerForInit( + @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull LockoutResetDispatcher lockoutResetDispatcher) { + return new BiometricScheduler<>(mHandler, BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties), gestureAvailabilityDispatcher, () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL, - new UserAwareBiometricScheduler.UserSwitchCallback() { + new UserSwitchProvider<IFingerprint, ISession>() { @NonNull @Override - public StopUserClient<?> getStopUserClient(int userId) { - return new FingerprintStopUserClient(mContext, mLazySession, mToken, + public StopUserClient<ISession> getStopUserClient(int userId) { + return new FingerprintStopUserClient(mContext, + () -> mLazySession.get().getSession(), mToken, userId, mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext, () -> mCurrentSession = null); @@ -152,69 +162,100 @@ public class Sensor { @NonNull @Override - public StartUserClient<?, ?> getStartUserClient(int newUserId) { + public StartUserClient<IFingerprint, ISession> getStartUserClient( + int newUserId) { final int sensorId = mSensorProperties.sensorId; - - final AidlResponseHandler resultController; - - if (Flags.deHidl()) { - resultController = new AidlResponseHandler( - mContext, mScheduler, sensorId, newUserId, - mLockoutTracker, lockoutResetDispatcher, - mBiometricContext.getAuthSessionCoordinator(), () -> {}, - new AidlResponseHandler.AidlResponseHandlerCallback() { - @Override - public void onEnrollSuccess() { - mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId, - newUserId); - mProvider.scheduleInvalidationRequest(sensorId, - newUserId); - } - - @Override - public void onHardwareUnavailable() { - Slog.e(mTag, - "Fingerprint sensor hardware unavailable."); - mCurrentSession = null; - } - }); - } else { - resultController = new AidlResponseHandler( - mContext, mScheduler, sensorId, newUserId, - mLockoutTracker, lockoutResetDispatcher, - mBiometricContext.getAuthSessionCoordinator(), () -> { - Slog.e(mTag, "Got ERROR_HW_UNAVAILABLE"); - mCurrentSession = null; - }); - } - - final StartUserClient.UserStartedCallback<ISession> userStartedCallback = - (userIdStarted, newSession, halInterfaceVersion) -> { - Slog.d(mTag, "New session created for user: " - + userIdStarted + " with hal version: " - + halInterfaceVersion); - mCurrentSession = new AidlSession(halInterfaceVersion, - newSession, userIdStarted, resultController); - if (FingerprintUtils.getInstance(sensorId) - .isInvalidationInProgress(mContext, userIdStarted)) { - Slog.w(mTag, - "Scheduling unfinished invalidation request for " - + "sensor: " - + sensorId - + ", user: " + userIdStarted); + final AidlResponseHandler resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> {}, + new AidlResponseHandler.AidlResponseHandlerCallback() { + @Override + public void onEnrollSuccess() { + mProvider.scheduleLoadAuthenticatorIdsForUser(sensorId, + newUserId); mProvider.scheduleInvalidationRequest(sensorId, - userIdStarted); + newUserId); + } + + @Override + public void onHardwareUnavailable() { + Slog.e(TAG, + "Fingerprint sensor hardware unavailable."); + mCurrentSession = null; } - }; + }); - return new FingerprintStartUserClient(mContext, mProvider::getHalInstance, - mToken, newUserId, mSensorProperties.sensorId, + return Sensor.this.getStartUserClient(resultController, sensorId, + newUserId); + } + }); + } + + private UserAwareBiometricScheduler<ISession, AidlSession> + getUserAwareBiometricSchedulerForInit( + GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + LockoutResetDispatcher lockoutResetDispatcher) { + return new UserAwareBiometricScheduler<>(TAG, + BiometricScheduler.sensorTypeFromFingerprintProperties(mSensorProperties), + gestureAvailabilityDispatcher, + () -> mCurrentSession != null ? mCurrentSession.getUserId() : UserHandle.USER_NULL, + new UserAwareBiometricScheduler.UserSwitchCallback() { + @NonNull + @Override + public StopUserClient<ISession> getStopUserClient(int userId) { + return new FingerprintStopUserClient(mContext, + () -> mLazySession.get().getSession(), mToken, + userId, mSensorProperties.sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext, - resultController, userStartedCallback); + () -> mCurrentSession = null); + } + + @NonNull + @Override + public StartUserClient<IFingerprint, ISession> getStartUserClient( + int newUserId) { + final int sensorId = mSensorProperties.sensorId; + + final AidlResponseHandler resultController = new AidlResponseHandler( + mContext, mScheduler, sensorId, newUserId, + mLockoutTracker, lockoutResetDispatcher, + mBiometricContext.getAuthSessionCoordinator(), () -> { + Slog.e(TAG, "Fingerprint hardware unavailable."); + mCurrentSession = null; + }); + + return Sensor.this.getStartUserClient(resultController, sensorId, + newUserId); } }); - mLockoutTracker = new LockoutCache(); - mLazySession = () -> mCurrentSession != null ? mCurrentSession : null; + } + + private FingerprintStartUserClient getStartUserClient(AidlResponseHandler resultController, + int sensorId, int newUserId) { + final StartUserClient.UserStartedCallback<ISession> userStartedCallback = + (userIdStarted, newSession, halInterfaceVersion) -> { + Slog.d(TAG, "New fingerprint session created for user: " + + userIdStarted + " with hal version: " + + halInterfaceVersion); + mCurrentSession = new AidlSession(halInterfaceVersion, + newSession, userIdStarted, resultController); + if (FingerprintUtils.getInstance(sensorId) + .isInvalidationInProgress(mContext, userIdStarted)) { + Slog.w(TAG, + "Scheduling unfinished invalidation request for " + + "fingerprint sensor: " + + sensorId + + ", user: " + userIdStarted); + mProvider.scheduleInvalidationRequest(sensorId, + userIdStarted); + } + }; + + return new FingerprintStartUserClient(mContext, mProvider::getHalInstance, + mToken, newUserId, mSensorProperties.sensorId, + BiometricLogger.ofUnknown(mContext), mBiometricContext, + resultController, userStartedCallback); } protected static FingerprintSensorPropertiesInternal getFingerprintSensorPropertiesInternal( @@ -267,7 +308,7 @@ public class Sensor { biometricStateCallback, mProvider, this); } - @NonNull public BiometricScheduler getScheduler() { + @NonNull public BiometricScheduler<IFingerprint, ISession> getScheduler() { return mScheduler; } @@ -283,17 +324,17 @@ public class Sensor { } void setTestHalEnabled(boolean enabled) { - Slog.w(mTag, "setTestHalEnabled: " + enabled); + Slog.w(TAG, "Fingerprint setTestHalEnabled: " + enabled); if (enabled != mTestHalEnabled) { // The framework should retrieve a new session from the HAL. try { if (mCurrentSession != null) { // TODO(181984005): This should be scheduled instead of directly invoked - Slog.d(mTag, "Closing old session"); + Slog.d(TAG, "Closing old fingerprint session"); mCurrentSession.getSession().close(); } } catch (RemoteException e) { - Slog.e(mTag, "RemoteException", e); + Slog.e(TAG, "RemoteException", e); } mCurrentSession = null; } @@ -335,7 +376,7 @@ public class Sensor { public void onBinderDied() { final BaseClientMonitor client = mScheduler.getCurrentClient(); if (client instanceof ErrorConsumer) { - Slog.e(mTag, "Sending ERROR_HW_UNAVAILABLE for client: " + client); + Slog.e(TAG, "Sending fingerprint hardware unavailable error for client: " + client); final ErrorConsumer errorConsumer = (ErrorConsumer) client; errorConsumer.onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index d3cecd0e34c7..4accf8f7ff30 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -119,7 +119,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider @NonNull private final AuthenticationStateListeners mAuthenticationStateListeners; private final ActivityTaskManager mActivityTaskManager; @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties; - private final BiometricScheduler mScheduler; + private final BiometricScheduler<IBiometricsFingerprint, AidlSession> mScheduler; private final Handler mHandler; private final LockoutResetDispatcher mLockoutResetDispatcher; private final LockoutFrameworkImpl mLockoutTracker; @@ -198,11 +198,11 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider private final int mSensorId; @NonNull private final Context mContext; @NonNull final Handler mHandler; - @NonNull final BiometricScheduler mScheduler; + @NonNull final BiometricScheduler<IBiometricsFingerprint, AidlSession> mScheduler; @Nullable private Callback mCallback; HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler, - @NonNull BiometricScheduler scheduler) { + @NonNull BiometricScheduler<IBiometricsFingerprint, AidlSession> scheduler) { mSensorId = sensorId; mContext = context; mHandler = handler; @@ -336,7 +336,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider @NonNull BiometricStateCallback biometricStateCallback, @NonNull AuthenticationStateListeners authenticationStateListeners, @NonNull FingerprintSensorPropertiesInternal sensorProps, - @NonNull BiometricScheduler scheduler, + @NonNull BiometricScheduler<IBiometricsFingerprint, AidlSession> scheduler, @NonNull Handler handler, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull HalResultController controller, @@ -389,8 +389,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider @NonNull Handler handler, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { - final BiometricScheduler scheduler = - new BiometricScheduler(TAG, + final BiometricScheduler<IBiometricsFingerprint, AidlSession> scheduler = + new BiometricScheduler<>( BiometricScheduler.sensorTypeFromFingerprintProperties(sensorProps), gestureAvailabilityDispatcher); final HalResultController controller = new HalResultController(sensorProps.sensorId, @@ -533,8 +533,8 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider private void scheduleUpdateActiveUserWithoutHandler(int targetUserId, boolean force) { final boolean hasEnrolled = !getEnrolledFingerprints(mSensorProperties.sensorId, targetUserId).isEmpty(); - final FingerprintUpdateActiveUserClient client = - new FingerprintUpdateActiveUserClient(mContext, mLazyDaemon, targetUserId, + final FingerprintUpdateActiveUserClientLegacy client = + new FingerprintUpdateActiveUserClientLegacy(mContext, mLazyDaemon, targetUserId, mContext.getOpPackageName(), mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN, BiometricsProtoEnums.CLIENT_UNKNOWN, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java index 88dae6fcc453..9232e11a05ee 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java @@ -140,9 +140,9 @@ public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManage private static class TestableBiometricScheduler extends BiometricScheduler { @NonNull private Fingerprint21UdfpsMock mFingerprint21; - TestableBiometricScheduler(@NonNull String tag, @NonNull Handler handler, + TestableBiometricScheduler( @Nullable GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { - super(tag, BiometricScheduler.SENSOR_TYPE_FP_OTHER, gestureAvailabilityDispatcher); + super(BiometricScheduler.SENSOR_TYPE_FP_OTHER, gestureAvailabilityDispatcher); } void init(@NonNull Fingerprint21UdfpsMock fingerprint21) { @@ -258,7 +258,7 @@ public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManage final Handler handler = new Handler(Looper.getMainLooper()); final TestableBiometricScheduler scheduler = - new TestableBiometricScheduler(TAG, handler, gestureAvailabilityDispatcher); + new TestableBiometricScheduler(gestureAvailabilityDispatcher); final MockHalResultController controller = new MockHalResultController(sensorProps.sensorId, context, handler, scheduler); return new Fingerprint21UdfpsMock(context, biometricStateCallback, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java index 5c5b9928f57a..59e64cd06667 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClient.java @@ -18,7 +18,7 @@ package com.android.server.biometrics.sensors.fingerprint.hidl; import android.annotation.NonNull; import android.content.Context; -import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; +import android.hardware.biometrics.fingerprint.ISession; import android.os.Build; import android.os.Environment; import android.os.RemoteException; @@ -39,8 +39,8 @@ import java.util.function.Supplier; /** * Sets the HAL's current active user, and updates the framework's authenticatorId cache. */ -public class FingerprintUpdateActiveUserClient extends - StartUserClient<IBiometricsFingerprint, AidlSession> { +public class FingerprintUpdateActiveUserClient extends StartUserClient<ISession, + AidlSession> { private static final String TAG = "FingerprintUpdateActiveUserClient"; private static final String FP_DATA_DIR = "fpdata"; @@ -52,19 +52,7 @@ public class FingerprintUpdateActiveUserClient extends private File mDirectory; FingerprintUpdateActiveUserClient(@NonNull Context context, - @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId, - @NonNull String owner, int sensorId, - @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, - @NonNull Supplier<Integer> currentUserId, - boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds, - boolean forceUpdateAuthenticatorId) { - this(context, lazyDaemon, userId, owner, sensorId, logger, biometricContext, currentUserId, - hasEnrolledBiometrics, authenticatorIds, forceUpdateAuthenticatorId, - (newUserId, newUser, halInterfaceVersion) -> {}); - } - - FingerprintUpdateActiveUserClient(@NonNull Context context, - @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId, + @NonNull Supplier<ISession> lazyDaemon, int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, @NonNull Supplier<Integer> currentUserId, @@ -132,9 +120,10 @@ public class FingerprintUpdateActiveUserClient extends try { final int targetId = getTargetUserId(); Slog.d(TAG, "Setting active user: " + targetId); - getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath()); + HidlToAidlSessionAdapter sessionAdapter = (HidlToAidlSessionAdapter) getFreshDaemon(); + sessionAdapter.setActiveGroup(targetId, mDirectory.getAbsolutePath()); mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics - ? getFreshDaemon().getAuthenticatorId() : 0L); + ? sessionAdapter.getAuthenticatorIdForUpdateClient() : 0L); mUserStartedCallback.onUserStarted(targetId, null, 0); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClientLegacy.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClientLegacy.java new file mode 100644 index 000000000000..fc85402edef4 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintUpdateActiveUserClientLegacy.java @@ -0,0 +1,133 @@ +/* + * 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.biometrics.sensors.fingerprint.hidl; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; +import android.os.Build; +import android.os.Environment; +import android.os.RemoteException; +import android.os.SELinux; +import android.util.Slog; + +import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.HalClientMonitor; + +import java.io.File; +import java.util.Map; +import java.util.function.Supplier; + +/** + * TODO(b/304604965): Delete this class once Flags.DE_HIDL is ready for release. + */ +public class FingerprintUpdateActiveUserClientLegacy extends + HalClientMonitor<IBiometricsFingerprint> { + private static final String TAG = "FingerprintUpdateActiveUserClient"; + private static final String FP_DATA_DIR = "fpdata"; + + private final Supplier<Integer> mCurrentUserId; + private final boolean mForceUpdateAuthenticatorId; + private final boolean mHasEnrolledBiometrics; + private final Map<Integer, Long> mAuthenticatorIds; + private File mDirectory; + + FingerprintUpdateActiveUserClientLegacy(@NonNull Context context, + @NonNull Supplier<IBiometricsFingerprint> lazyDaemon, int userId, + @NonNull String owner, int sensorId, + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + @NonNull Supplier<Integer> currentUserId, + boolean hasEnrolledBiometrics, @NonNull Map<Integer, Long> authenticatorIds, + boolean forceUpdateAuthenticatorId) { + super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, + 0 /* cookie */, sensorId, logger, biometricContext); + mCurrentUserId = currentUserId; + mForceUpdateAuthenticatorId = forceUpdateAuthenticatorId; + mHasEnrolledBiometrics = hasEnrolledBiometrics; + mAuthenticatorIds = authenticatorIds; + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + + if (mCurrentUserId.get() == getTargetUserId() && !mForceUpdateAuthenticatorId) { + Slog.d(TAG, "Already user: " + mCurrentUserId + ", returning"); + callback.onClientFinished(this, true /* success */); + return; + } + + int firstSdkInt = Build.VERSION.DEVICE_INITIAL_SDK_INT; + if (firstSdkInt < Build.VERSION_CODES.BASE) { + Slog.e(TAG, "First SDK version " + firstSdkInt + " is invalid; must be " + + "at least VERSION_CODES.BASE"); + } + File baseDir; + if (firstSdkInt <= Build.VERSION_CODES.O_MR1) { + baseDir = Environment.getUserSystemDirectory(getTargetUserId()); + } else { + baseDir = Environment.getDataVendorDeDirectory(getTargetUserId()); + } + + mDirectory = new File(baseDir, FP_DATA_DIR); + if (!mDirectory.exists()) { + if (!mDirectory.mkdir()) { + Slog.e(TAG, "Cannot make directory: " + mDirectory.getAbsolutePath()); + callback.onClientFinished(this, false /* success */); + return; + } + // Calling mkdir() from this process will create a directory with our + // permissions (inherited from the containing dir). This command fixes + // the label. + if (!SELinux.restorecon(mDirectory)) { + Slog.e(TAG, "Restorecons failed. Directory will have wrong label."); + callback.onClientFinished(this, false /* success */); + return; + } + } + + startHalOperation(); + } + + @Override + public void unableToStart() { + // Nothing to do here + } + + @Override + protected void startHalOperation() { + try { + final int targetId = getTargetUserId(); + Slog.d(TAG, "Setting active user: " + targetId); + getFreshDaemon().setActiveGroup(targetId, mDirectory.getAbsolutePath()); + mAuthenticatorIds.put(targetId, mHasEnrolledBiometrics + ? getFreshDaemon().getAuthenticatorId() : 0L); + mCallback.onClientFinished(this, true /* success */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to setActiveGroup: " + e); + mCallback.onClientFinished(this, false /* success */); + } + } + + @Override + public int getProtoEnum() { + return BiometricsProto.CM_UPDATE_ACTIVE_USER; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java index 90da74ccaa1c..47fdcdb92a99 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.pm.UserInfo; +import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.os.Handler; @@ -39,7 +40,7 @@ import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.StartUserClient; import com.android.server.biometrics.sensors.StopUserClient; -import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.UserSwitchProvider; import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler; @@ -71,37 +72,33 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } }; - public HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider, - @NonNull Context context, @NonNull Handler handler, + public HidlToAidlSensorAdapter(@NonNull FingerprintProvider provider, + @NonNull Context context, + @NonNull Handler handler, @NonNull SensorProps prop, @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, boolean resetLockoutRequiresHardwareAuthToken, @NonNull Runnable internalCleanupRunnable) { - this(tag, provider, context, handler, prop, lockoutResetDispatcher, - gestureAvailabilityDispatcher, biometricContext, + this(provider, context, handler, prop, lockoutResetDispatcher, biometricContext, resetLockoutRequiresHardwareAuthToken, internalCleanupRunnable, new AuthSessionCoordinator(), null /* daemon */, null /* onEnrollSuccessCallback */); } @VisibleForTesting - HidlToAidlSensorAdapter(@NonNull String tag, @NonNull FingerprintProvider provider, + HidlToAidlSensorAdapter(@NonNull FingerprintProvider provider, @NonNull Context context, @NonNull Handler handler, @NonNull SensorProps prop, @NonNull LockoutResetDispatcher lockoutResetDispatcher, - @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, @NonNull BiometricContext biometricContext, boolean resetLockoutRequiresHardwareAuthToken, @NonNull Runnable internalCleanupRunnable, @NonNull AuthSessionCoordinator authSessionCoordinator, @Nullable IBiometricsFingerprint daemon, @Nullable AidlResponseHandler.AidlResponseHandlerCallback aidlResponseHandlerCallback) { - super(tag, provider, context, handler, getFingerprintSensorPropertiesInternal(prop, + super(provider, context, handler, getFingerprintSensorPropertiesInternal(prop, new ArrayList<>(), resetLockoutRequiresHardwareAuthToken), - lockoutResetDispatcher, - gestureAvailabilityDispatcher, biometricContext, null /* session */); mLockoutResetDispatcher = lockoutResetDispatcher; mInternalCleanupRunnable = internalCleanupRunnable; @@ -127,7 +124,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe @Override public void serviceDied(long cookie) { - Slog.d(TAG, "HAL died."); + Slog.d(TAG, "Fingerprint HAL died."); mSession = null; mDaemon = null; } @@ -139,12 +136,12 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } @Override - public void init(GestureAvailabilityDispatcher gestureAvailabilityDispatcher, - LockoutResetDispatcher lockoutResetDispatcher) { + public void init(@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher, + @NonNull LockoutResetDispatcher lockoutResetDispatcher) { setLazySession(this::getSession); - setScheduler(new UserAwareBiometricScheduler(TAG, + setScheduler(new BiometricScheduler<ISession, AidlSession>(getHandler(), BiometricScheduler.sensorTypeFromFingerprintProperties(getSensorProperties()), - gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback())); + gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchProvider())); mLockoutTracker = new LockoutFrameworkImpl(getContext(), userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks( getSensorProperties().sensorId), getHandler()); @@ -152,6 +149,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe @Override @Nullable + @VisibleForTesting protected AidlSession getSessionForUser(int userId) { if (mSession != null && mSession.getUserId() == userId) { return mSession; @@ -217,21 +215,18 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } mDaemon.asBinder().linkToDeath(this, 0 /* flags */); - - Slog.d(TAG, "Fingerprint HAL ready"); - scheduleLoadAuthenticatorIds(); mInternalCleanupRunnable.run(); return mDaemon; } - private UserAwareBiometricScheduler.UserSwitchCallback getUserSwitchCallback() { - return new UserAwareBiometricScheduler.UserSwitchCallback() { + private UserSwitchProvider<ISession, AidlSession> getUserSwitchProvider() { + return new UserSwitchProvider<>() { @NonNull @Override - public StopUserClient<?> getStopUserClient(int userId) { - return new StopUserClient<IBiometricsFingerprint>(getContext(), - HidlToAidlSensorAdapter.this::getIBiometricsFingerprint, + public StopUserClient<AidlSession> getStopUserClient(int userId) { + return new StopUserClient<>(getContext(), + HidlToAidlSensorAdapter.this::getSession, null /* token */, userId, getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()), getBiometricContext(), () -> { @@ -258,7 +253,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe @NonNull @Override - public StartUserClient<?, ?> getStartUserClient(int newUserId) { + public StartUserClient<ISession, AidlSession> getStartUserClient(int newUserId) { return getFingerprintUpdateActiveUserClient(newUserId, false /* forceUpdateAuthenticatorId */); } @@ -268,7 +263,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe private FingerprintUpdateActiveUserClient getFingerprintUpdateActiveUserClient(int newUserId, boolean forceUpdateAuthenticatorIds) { return new FingerprintUpdateActiveUserClient(getContext(), - this::getIBiometricsFingerprint, newUserId, TAG, + () -> getSession().getSession(), newUserId, TAG, getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()), getBiometricContext(), () -> mCurrentUserId, !FingerprintUtils.getInstance(getSensorProperties().sensorId) @@ -290,7 +285,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } @VisibleForTesting void handleUserChanged(int newUserId) { - Slog.d(TAG, "User changed. Current user is " + newUserId); + Slog.d(TAG, "User changed. Current user for fingerprint sensor is " + newUserId); mSession = null; mCurrentUserId = newUserId; } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java index 2fc00e126354..b469752d49cf 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSessionAdapter.java @@ -209,6 +209,14 @@ public class HidlToAidlSessionAdapter implements ISession { return null; } + public long getAuthenticatorIdForUpdateClient() throws RemoteException { + return mSession.get().getAuthenticatorId(); + } + + public void setActiveGroup(int userId, String absolutePath) throws RemoteException { + mSession.get().setActiveGroup(userId, absolutePath); + } + private void setCallback(AidlResponseHandler aidlResponseHandler) { mHidlToAidlCallbackConverter = new HidlToAidlCallbackConverter(aidlResponseHandler); try { diff --git a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java index 08143759fab4..816c3490d0a0 100644 --- a/services/core/java/com/android/server/criticalevents/CriticalEventLog.java +++ b/services/core/java/com/android/server/criticalevents/CriticalEventLog.java @@ -29,6 +29,7 @@ import com.android.server.criticalevents.nano.CriticalEventLogStorageProto; import com.android.server.criticalevents.nano.CriticalEventProto; import com.android.server.criticalevents.nano.CriticalEventProto.AppNotResponding; import com.android.server.criticalevents.nano.CriticalEventProto.HalfWatchdog; +import com.android.server.criticalevents.nano.CriticalEventProto.InstallPackages; import com.android.server.criticalevents.nano.CriticalEventProto.JavaCrash; import com.android.server.criticalevents.nano.CriticalEventProto.NativeCrash; import com.android.server.criticalevents.nano.CriticalEventProto.SystemServerStarted; @@ -142,6 +143,13 @@ public class CriticalEventLog { return System.currentTimeMillis(); } + /** Logs when one or more packages are installed. */ + public void logInstallPackagesStarted() { + CriticalEventProto event = new CriticalEventProto(); + event.setInstallPackages(new InstallPackages()); + log(event); + } + /** Logs when system server started. */ public void logSystemServerStarted() { CriticalEventProto event = new CriticalEventProto(); diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java index 7cea9c422b4d..e54f30fa29f1 100644 --- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java +++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java @@ -27,6 +27,7 @@ import android.view.DisplayAddress; import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.config.layout.Layouts; import com.android.server.display.config.layout.XmlParser; +import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.layout.DisplayIdProducer; import com.android.server.display.layout.Layout; @@ -68,12 +69,15 @@ class DeviceStateToLayoutMap { private final SparseArray<Layout> mLayoutMap = new SparseArray<>(); private final DisplayIdProducer mIdProducer; + private final boolean mIsPortInDisplayLayoutEnabled; - DeviceStateToLayoutMap(DisplayIdProducer idProducer) { - this(idProducer, getConfigFile()); + DeviceStateToLayoutMap(DisplayIdProducer idProducer, DisplayManagerFlags flags) { + this(idProducer, flags, getConfigFile()); } - DeviceStateToLayoutMap(DisplayIdProducer idProducer, File configFile) { + DeviceStateToLayoutMap(DisplayIdProducer idProducer, DisplayManagerFlags flags, + File configFile) { + mIsPortInDisplayLayoutEnabled = flags.isPortInDisplayLayoutEnabled(); mIdProducer = idProducer; loadLayoutsFromConfig(configFile); createLayout(STATE_DEFAULT); @@ -93,6 +97,7 @@ class DeviceStateToLayoutMap { ipw.println("DeviceStateToLayoutMap:"); ipw.increaseIndent(); + ipw.println("mIsPortInDisplayLayoutEnabled=" + mIsPortInDisplayLayoutEnabled); ipw.println("Registered Layouts:"); for (int i = 0; i < mLayoutMap.size(); i++) { ipw.println("state(" + mLayoutMap.keyAt(i) + "): " + mLayoutMap.valueAt(i)); @@ -132,13 +137,15 @@ class DeviceStateToLayoutMap { final Layout layout = createLayout(state); for (com.android.server.display.config.layout.Display d: l.getDisplay()) { assert layout != null; + final DisplayAddress address = getDisplayAddressForLayoutDisplay(d); + int position = getPosition(d.getPosition()); BigInteger leadDisplayPhysicalId = d.getLeadDisplayAddress(); DisplayAddress leadDisplayAddress = leadDisplayPhysicalId == null ? null : DisplayAddress.fromPhysicalDisplayId( leadDisplayPhysicalId.longValue()); layout.createDisplayLocked( - DisplayAddress.fromPhysicalDisplayId(d.getAddress().longValue()), + address, d.isDefaultDisplay(), d.isEnabled(), d.getDisplayGroup(), @@ -158,6 +165,20 @@ class DeviceStateToLayoutMap { } } + private DisplayAddress getDisplayAddressForLayoutDisplay( + @NonNull com.android.server.display.config.layout.Display display) { + BigInteger xmlAddress = display.getAddress_optional(); + if (xmlAddress != null) { + return DisplayAddress.fromPhysicalDisplayId(xmlAddress.longValue()); + } + if (!mIsPortInDisplayLayoutEnabled || display.getPort_optional() == null) { + throw new IllegalArgumentException( + "Must specify a display identifier in display layout configuration: " + display); + } + return DisplayAddress.fromPortAndModel((int) display.getPort_optional().longValue(), + /* model= */ null); + } + private int getPosition(@NonNull String position) { int positionInt = POSITION_UNKNOWN; if (FRONT_STRING.equals(position)) { diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index 9fcaa1e2af16..d50a43aa93d1 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -33,6 +33,7 @@ public final class DisplayBrightnessState { private final float mSdrBrightness; private final float mMaxBrightness; + private final float mMinBrightness; private final BrightnessReason mBrightnessReason; private final String mDisplayBrightnessStrategyName; private final boolean mShouldUseAutoBrightness; @@ -50,6 +51,7 @@ public final class DisplayBrightnessState { mShouldUseAutoBrightness = builder.getShouldUseAutoBrightness(); mIsSlowChange = builder.isSlowChange(); mMaxBrightness = builder.getMaxBrightness(); + mMinBrightness = builder.getMinBrightness(); mCustomAnimationRate = builder.getCustomAnimationRate(); mShouldUpdateScreenBrightnessSetting = builder.shouldUpdateScreenBrightnessSetting(); } @@ -105,6 +107,13 @@ public final class DisplayBrightnessState { } /** + * @return minimum allowed brightness + */ + public float getMinBrightness() { + return mMinBrightness; + } + + /** * @return custom animation rate */ public float getCustomAnimationRate() { @@ -131,6 +140,7 @@ public final class DisplayBrightnessState { stringBuilder.append(getShouldUseAutoBrightness()); stringBuilder.append("\n isSlowChange:").append(mIsSlowChange); stringBuilder.append("\n maxBrightness:").append(mMaxBrightness); + stringBuilder.append("\n minBrightness:").append(mMinBrightness); stringBuilder.append("\n customAnimationRate:").append(mCustomAnimationRate); stringBuilder.append("\n shouldUpdateScreenBrightnessSetting:") .append(mShouldUpdateScreenBrightnessSetting); @@ -160,6 +170,7 @@ public final class DisplayBrightnessState { && mShouldUseAutoBrightness == otherState.getShouldUseAutoBrightness() && mIsSlowChange == otherState.isSlowChange() && mMaxBrightness == otherState.getMaxBrightness() + && mMinBrightness == otherState.getMinBrightness() && mCustomAnimationRate == otherState.getCustomAnimationRate() && mShouldUpdateScreenBrightnessSetting == otherState.shouldUpdateScreenBrightnessSetting(); @@ -168,7 +179,8 @@ public final class DisplayBrightnessState { @Override public int hashCode() { return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason, - mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mCustomAnimationRate, + mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness, + mCustomAnimationRate, mShouldUpdateScreenBrightnessSetting); } @@ -190,6 +202,7 @@ public final class DisplayBrightnessState { private boolean mShouldUseAutoBrightness; private boolean mIsSlowChange; private float mMaxBrightness; + private float mMinBrightness; private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET; private boolean mShouldUpdateScreenBrightnessSetting; @@ -208,6 +221,7 @@ public final class DisplayBrightnessState { builder.setShouldUseAutoBrightness(state.getShouldUseAutoBrightness()); builder.setIsSlowChange(state.isSlowChange()); builder.setMaxBrightness(state.getMaxBrightness()); + builder.setMinBrightness(state.getMinBrightness()); builder.setCustomAnimationRate(state.getCustomAnimationRate()); builder.setShouldUpdateScreenBrightnessSetting( state.shouldUpdateScreenBrightnessSetting()); @@ -334,6 +348,20 @@ public final class DisplayBrightnessState { return mMaxBrightness; } + /** + * See {@link DisplayBrightnessState#getMinBrightness()}. + */ + public Builder setMinBrightness(float minBrightness) { + this.mMinBrightness = minBrightness; + return this; + } + + /** + * See {@link DisplayBrightnessState#getMinBrightness()}. + */ + public float getMinBrightness() { + return mMinBrightness; + } /** * See {@link DisplayBrightnessState#getCustomAnimationRate()}. diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index bd22e1d21dea..4c4cf6080965 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -16,6 +16,7 @@ package com.android.server.display; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS; import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat; import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat; @@ -567,7 +568,8 @@ public class DisplayDeviceConfig { public static final int DEFAULT_LOW_REFRESH_RATE = 60; - private static final float BRIGHTNESS_DEFAULT = 0.5f; + @VisibleForTesting + static final float BRIGHTNESS_DEFAULT = 0.5f; private static final String ETC_DIR = "etc"; private static final String DISPLAY_CONFIG_DIR = "displayconfig"; private static final String CONFIG_FILE_FORMAT = "display_%s.xml"; @@ -597,8 +599,6 @@ public class DisplayDeviceConfig { // so -2 is used instead private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f; - static final float NITS_INVALID = -1; - // Length of the ambient light horizon used to calculate the long term estimate of ambient // light. private static final int AMBIENT_LIGHT_LONG_HORIZON_MILLIS = 10000; @@ -1031,11 +1031,12 @@ public class DisplayDeviceConfig { /** * Calculates the nits value for the specified backlight value if a mapping exists. * - * @return The mapped nits or {@link #NITS_INVALID} if no mapping exits. + * @return The mapped nits or {@link BrightnessMappingStrategy.INVALID_NITS} if no mapping + * exits. */ public float getNitsFromBacklight(float backlight) { if (mBacklightToNitsSpline == null) { - return NITS_INVALID; + return INVALID_NITS; } backlight = Math.max(backlight, mBacklightMinimum); return mBacklightToNitsSpline.interpolate(backlight); @@ -1061,7 +1062,7 @@ public class DisplayDeviceConfig { float backlight = getBacklightFromBrightness(brightness); float nits = getNitsFromBacklight(backlight); - if (nits == NITS_INVALID) { + if (nits == INVALID_NITS) { return PowerManager.BRIGHTNESS_INVALID; } diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java index 67e612d1fd99..6164154b1e63 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java +++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java @@ -129,7 +129,9 @@ class DisplayDeviceRepository implements DisplayAdapter.Listener { public DisplayDevice getByAddressLocked(@NonNull DisplayAddress address) { for (int i = mDisplayDevices.size() - 1; i >= 0; i--) { final DisplayDevice device = mDisplayDevices.get(i); - if (address.equals(device.getDisplayDeviceInfoLocked().address)) { + final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + if (address.equals(info.address) + || DisplayAddress.Physical.isPortMatch(address, info.address)) { return device; } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index bc3f9dd3cb8c..fbac924be283 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1615,6 +1615,10 @@ public final class DisplayManagerService extends SystemService { if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) { Slog.w(TAG, "Display created with home support but lacks " + "VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the home support request."); + } else if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { + Slog.w(TAG, "Display created with home support but has " + + "VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, ignoring the home support " + + "request."); } else { mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId, Display.TYPE_VIRTUAL, true); diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 7df61142475a..2d860c0cc673 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -573,10 +573,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mBrightnessClamperController = mInjector.getBrightnessClamperController( mHandler, modeChangeCallback::run, new BrightnessClamperController.DisplayDeviceData( - mUniqueDisplayId, - mThermalBrightnessThrottlingDataId, - logicalDisplay.getPowerThrottlingDataIdLocked(), - mDisplayDeviceConfig), mContext, flags); + mUniqueDisplayId, + mThermalBrightnessThrottlingDataId, + logicalDisplay.getPowerThrottlingDataIdLocked(), + mDisplayDeviceConfig), mContext, flags); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); mAutomaticBrightnessStrategy = @@ -1508,7 +1508,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // Note throttling effectively changes the allowed brightness range, so, similarly to HBM, // we broadcast this change through setting. final float unthrottledBrightnessState = brightnessState; - DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest, brightnessState, slowChange); @@ -1522,11 +1521,12 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal if (updateScreenBrightnessSetting) { // Tell the rest of the system about the new brightness in case we had to change it // for things like auto-brightness or high-brightness-mode. Note that we do this - // only considering maxBrightness (ignroing brightness modifiers like low power or dim) + // only considering maxBrightness (ignoring brightness modifiers like low power or dim) // so that the slider accurately represents the full possible range, // even if they range changes what it means in absolute terms. mDisplayBrightnessController.updateScreenBrightnessSetting( - Math.min(unthrottledBrightnessState, clampedState.getMaxBrightness())); + MathUtils.constrain(unthrottledBrightnessState, + clampedState.getMinBrightness(), clampedState.getMaxBrightness())); } // The current brightness to use has been calculated at this point, and HbmController should @@ -1935,8 +1935,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal @Nullable DisplayBrightnessState state) { synchronized (mCachedBrightnessInfo) { float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX; - final float minBrightness = Math.min( - mBrightnessRangeController.getCurrentBrightnessMin(), stateMax); + float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX; + final float minBrightness = Math.max(stateMin, Math.min( + mBrightnessRangeController.getCurrentBrightnessMin(), stateMax)); final float maxBrightness = Math.min( mBrightnessRangeController.getCurrentBrightnessMax(), stateMax); boolean changed = false; @@ -1962,7 +1963,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal changed |= mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason, mBrightnessClamperController.getBrightnessMaxReason()); - return changed; } } @@ -2880,6 +2880,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0, mBrightnessClamperController.getBrightnessMaxReason(), + // TODO: (flc) add brightnessMinReason here too. (modifier & BrightnessReason.MODIFIER_DIMMED) > 0, event.isRbcEnabled(), (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0, diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java index bcf27b4e8f0a..90bad12869f4 100644 --- a/services/core/java/com/android/server/display/DisplayPowerState.java +++ b/services/core/java/com/android/server/display/DisplayPowerState.java @@ -333,6 +333,8 @@ final class DisplayPowerState { public void stop() { mStopped = true; mPhotonicModulator.interrupt(); + mColorFadePrepared = false; + mColorFadeReady = true; if (mColorFade != null) { mAsyncDestroyExecutor.execute(mColorFade::destroy); } @@ -419,7 +421,8 @@ final class DisplayPowerState { } }; - private final Runnable mColorFadeDrawRunnable = new Runnable() { + @VisibleForTesting + final Runnable mColorFadeDrawRunnable = new Runnable() { @Override public void run() { mColorFadeDrawPending = false; diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 25576ce9efd6..3a6333099b77 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -19,6 +19,8 @@ package com.android.server.display; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.Mode.INVALID_MODE_ID; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS; + import android.annotation.Nullable; import android.app.ActivityThread; import android.content.Context; @@ -956,8 +958,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { void handleHdrSdrNitsChanged(float displayNits, float sdrNits) { final float newHdrSdrRatio; - if (displayNits != DisplayDeviceConfig.NITS_INVALID - && sdrNits != DisplayDeviceConfig.NITS_INVALID) { + if (displayNits != INVALID_NITS && sdrNits != INVALID_NITS) { // Ensure the ratio stays >= 1.0f as values below that are nonsensical newHdrSdrRatio = Math.max(1.f, displayNits / sdrNits); } else { diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 115111a4afa3..2e8de31f2af1 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -205,7 +205,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { @NonNull Handler handler, DisplayManagerFlags flags) { this(context, foldSettingProvider, repo, listener, syncRoot, handler, new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY - : sNextNonDefaultDisplayId++), flags); + : sNextNonDefaultDisplayId++, flags), flags); } LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider, @@ -1094,8 +1094,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final DisplayAddress address = displayLayout.getAddress(); final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address); if (device == null) { - Slog.w(TAG, "The display device (" + address + "), is not available" - + " for the display state " + mDeviceState); + Slog.w(TAG, "applyLayoutLocked: The display device (" + address + "), is not " + + "available for the display state " + mDeviceState); continue; } diff --git a/services/core/java/com/android/server/display/brightness/BrightnessReason.java b/services/core/java/com/android/server/display/brightness/BrightnessReason.java index 8fe5f213d766..bc443a8167ab 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessReason.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessReason.java @@ -46,8 +46,10 @@ public final class BrightnessReason { public static final int MODIFIER_LOW_POWER = 0x2; public static final int MODIFIER_HDR = 0x4; public static final int MODIFIER_THROTTLED = 0x8; + public static final int MODIFIER_MIN_LUX = 0x10; + public static final int MODIFIER_MIN_USER_SET_LOWER_BOUND = 0x20; public static final int MODIFIER_MASK = MODIFIER_DIMMED | MODIFIER_LOW_POWER | MODIFIER_HDR - | MODIFIER_THROTTLED; + | MODIFIER_THROTTLED | MODIFIER_MIN_LUX | MODIFIER_MIN_USER_SET_LOWER_BOUND; // ADJUSTMENT_* // These things can happen at any point, even if the main brightness reason doesn't @@ -131,6 +133,12 @@ public final class BrightnessReason { if ((mModifier & MODIFIER_THROTTLED) != 0) { sb.append(" throttled"); } + if ((mModifier & MODIFIER_MIN_LUX) != 0) { + sb.append(" lux_lower_bound"); + } + if ((mModifier & MODIFIER_MIN_USER_SET_LOWER_BOUND) != 0) { + sb.append(" user_min_pref"); + } int strlen = sb.length(); if (sb.charAt(strlen - 1) == '[') { sb.setLength(strlen - 2); diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java index 42ebc401335e..fab769e8bc4f 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java @@ -30,6 +30,7 @@ import java.io.PrintWriter; abstract class BrightnessClamper<T> { protected float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; + protected boolean mIsActive = false; @NonNull @@ -75,6 +76,5 @@ abstract class BrightnessClamper<T> { THERMAL, POWER, BEDTIME_MODE, - LUX, } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index 01694ddee06a..2c02fc610a51 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -58,13 +58,14 @@ public class BrightnessClamperController { private final Executor mExecutor; private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers; - private final List<BrightnessModifier> mModifiers; + private final List<BrightnessStateModifier> mModifiers; private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; private float mCustomAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; @Nullable private Type mClamperType = null; + private boolean mClamperApplied = false; public BrightnessClamperController(Handler handler, @@ -92,7 +93,7 @@ public class BrightnessClamperController { mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags, context); - mModifiers = injector.getModifiers(context); + mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener); mOnPropertiesChangedListener = properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); start(); @@ -165,9 +166,10 @@ public class BrightnessClamperController { * Used to dump ClampersController state. */ public void dump(PrintWriter writer) { - writer.println("BrightnessClampersController:"); + writer.println("BrightnessClamperController:"); writer.println(" mBrightnessCap: " + mBrightnessCap); writer.println(" mClamperType: " + mClamperType); + writer.println(" mClamperApplied: " + mClamperApplied); IndentingPrintWriter ipw = new IndentingPrintWriter(writer, " "); mClampers.forEach(clamper -> clamper.dump(ipw)); mModifiers.forEach(modifier -> modifier.dump(ipw)); @@ -181,6 +183,7 @@ public class BrightnessClamperController { mDeviceConfigParameterProvider.removeOnPropertiesChangedListener( mOnPropertiesChangedListener); mClampers.forEach(BrightnessClamper::stop); + mModifiers.forEach(BrightnessStateModifier::stop); } @@ -201,14 +204,14 @@ public class BrightnessClamperController { customAnimationRate = minClamper.getCustomAnimationRate(); } - if (mBrightnessCap != brightnessCap || mClamperType != clamperType + if (mBrightnessCap != brightnessCap + || mClamperType != clamperType || mCustomAnimationRate != customAnimationRate) { mBrightnessCap = brightnessCap; mClamperType = clamperType; mCustomAnimationRate = customAnimationRate; mClamperChangeListenerExternal.onChanged(); } - } private void start() { @@ -248,16 +251,17 @@ public class BrightnessClamperController { clampers.add(new BrightnessWearBedtimeModeClamper(handler, context, clamperChangeListener, data)); } - if (flags.isEvenDimmerEnabled()) { - clampers.add(new BrightnessMinClamper(handler, clamperChangeListener, context)); - } return clampers; } - List<BrightnessModifier> getModifiers(Context context) { - List<BrightnessModifier> modifiers = new ArrayList<>(); + List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, + Handler handler, ClamperChangeListener listener) { + List<BrightnessStateModifier> modifiers = new ArrayList<>(); modifiers.add(new DisplayDimModifier(context)); modifiers.add(new BrightnessLowPowerModeModifier()); + if (flags.isEvenDimmerEnabled()) { + modifiers.add(new BrightnessLowLuxModifier(handler, listener, context)); + } return modifiers; } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java new file mode 100644 index 000000000000..7f1f7a99e438 --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java @@ -0,0 +1,176 @@ +/* + * 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.display.brightness.clamper; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.hardware.display.DisplayManagerInternal; +import android.net.Uri; +import android.os.Handler; +import android.os.PowerManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; +import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.utils.DebugUtils; + +import java.io.PrintWriter; + +/** + * Class used to prevent the screen brightness dipping below a certain value, based on current + * lux conditions and user preferred minimum. + */ +public class BrightnessLowLuxModifier implements + BrightnessStateModifier { + + // To enable these logs, run: + // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot' + private static final String TAG = "BrightnessLowLuxModifier"; + private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); + private final SettingsObserver mSettingsObserver; + private final ContentResolver mContentResolver; + private final Handler mHandler; + private final BrightnessClamperController.ClamperChangeListener mChangeListener; + protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN; + private int mReason; + private float mBrightnessLowerBound; + private boolean mIsActive; + + @VisibleForTesting + BrightnessLowLuxModifier(Handler handler, + BrightnessClamperController.ClamperChangeListener listener, Context context) { + super(); + + mChangeListener = listener; + mHandler = handler; + mContentResolver = context.getContentResolver(); + mSettingsObserver = new SettingsObserver(mHandler); + mHandler.post(() -> { + start(); + }); + } + + /** + * Calculates new lower bound for brightness range, based on whether the setting is active, + * the user defined min brightness setting, and current lux environment. + */ + @VisibleForTesting + public void recalculateLowerBound() { + int userId = UserHandle.USER_CURRENT; + float settingNitsLowerBound = Settings.Secure.getFloatForUser( + mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, + /* def= */ PowerManager.BRIGHTNESS_MIN, userId); + + boolean isActive = Settings.Secure.getIntForUser(mContentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, + /* def= */ 0, userId) == 1; + + // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux); + float luxBasedNitsLowerBound = 0.0f; + + // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound, + // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN; + + final int reason = settingNitsLowerBound > luxBasedNitsLowerBound + ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND + : BrightnessReason.MODIFIER_MIN_LUX; + + // TODO: brightnessLowerBound = nitsToBrightnessSpline(nitsLowerBound); + final float brightnessLowerBound = PowerManager.BRIGHTNESS_MIN; + + if (mBrightnessLowerBound != brightnessLowerBound + || mReason != reason + || mIsActive != isActive) { + mIsActive = isActive; + mReason = reason; + if (DEBUG) { + Slog.i(TAG, "isActive: " + isActive + + ", settingNitsLowerBound: " + settingNitsLowerBound + + ", lowerBound: " + brightnessLowerBound); + } + mBrightnessLowerBound = brightnessLowerBound; + mChangeListener.onChanged(); + } + } + + @VisibleForTesting + public boolean isActive() { + return mIsActive; + } + + @VisibleForTesting + public int getBrightnessReason() { + return mReason; + } + + @VisibleForTesting + public float getBrightnessLowerBound() { + return mBrightnessLowerBound; + } + + void start() { + recalculateLowerBound(); + } + + @Override + public void apply(DisplayManagerInternal.DisplayPowerRequest request, + DisplayBrightnessState.Builder stateBuilder) { + stateBuilder.setMinBrightness(mBrightnessLowerBound); + float boundedBrightness = Math.max(mBrightnessLowerBound, stateBuilder.getBrightness()); + stateBuilder.setBrightness(boundedBrightness); + + if (BrightnessSynchronizer.floatEquals(stateBuilder.getBrightness(), + mBrightnessLowerBound)) { + stateBuilder.getBrightnessReason().addModifier(mReason); + } + } + + @Override + public void stop() { + mContentResolver.unregisterContentObserver(mSettingsObserver); + } + + @Override + public void dump(PrintWriter pw) { + pw.println("BrightnessLowLuxModifier:"); + pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound); + pw.println(" mIsActive=" + mIsActive); + pw.println(" mReason=" + mReason); + } + + private final class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS), + false, this); + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED), + false, this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + recalculateLowerBound(); + } + } +} diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java deleted file mode 100644 index 71efca12f91c..000000000000 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java +++ /dev/null @@ -1,137 +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.display.brightness.clamper; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.os.PowerManager; -import android.os.UserHandle; -import android.provider.Settings; -import android.util.Slog; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.server.display.utils.DebugUtils; - -import java.io.PrintWriter; - -/** - * Class used to prevent the screen brightness dipping below a certain value, based on current - * lux conditions. - */ -public class BrightnessMinClamper extends BrightnessClamper { - - // To enable these logs, run: - // 'adb shell setprop persist.log.tag.BrightnessMinClamper DEBUG && adb reboot' - private static final String TAG = "BrightnessMinClamper"; - private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); - - private final SettingsObserver mSettingsObserver; - - ContentResolver mContentResolver; - private float mNitsLowerBound; - - @VisibleForTesting - BrightnessMinClamper(Handler handler, - BrightnessClamperController.ClamperChangeListener listener, Context context) { - super(handler, listener); - - mContentResolver = context.getContentResolver(); - mSettingsObserver = new SettingsObserver(mHandler); - mHandler.post(() -> { - start(); - }); - } - - private void recalculateLowerBound() { - final int userId = UserHandle.USER_CURRENT; - float settingNitsLowerBound = Settings.Secure.getFloatForUser( - mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, - /* def= */ PowerManager.BRIGHTNESS_MIN, userId); - - boolean isActive = Settings.Secure.getIntForUser(mContentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, - /* def= */ 0, userId) == 1; - - // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux); - float luxBasedNitsLowerBound = PowerManager.BRIGHTNESS_MIN; - final float nitsLowerBound = Math.max(settingNitsLowerBound, luxBasedNitsLowerBound); - - if (mNitsLowerBound != nitsLowerBound || mIsActive != isActive) { - mIsActive = isActive; - mNitsLowerBound = nitsLowerBound; - if (DEBUG) { - Slog.i(TAG, "mIsActive: " + mIsActive); - } - // TODO: mBrightnessCap = nitsToBrightnessSpline(mNitsLowerBound); - mChangeListener.onChanged(); - } - } - - void start() { - recalculateLowerBound(); - } - - - @Override - Type getType() { - return Type.LUX; - } - - @Override - void onDeviceConfigChanged() { - // TODO - } - - @Override - void onDisplayChanged(Object displayData) { - - } - - @Override - void stop() { - mContentResolver.unregisterContentObserver(mSettingsObserver); - } - - @Override - void dump(PrintWriter pw) { - pw.println("BrightnessMinClamper:"); - pw.println(" mBrightnessCap=" + mBrightnessCap); - pw.println(" mIsActive=" + mIsActive); - pw.println(" mNitsLowerBound=" + mNitsLowerBound); - super.dump(pw); - } - - private final class SettingsObserver extends ContentObserver { - SettingsObserver(Handler handler) { - super(handler); - mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS), - false, this); - mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED), - false, this); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - recalculateLowerBound(); - } - } -} diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java index 112e63dc62d4..be8fa5a0f0ce 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java @@ -26,7 +26,7 @@ import java.io.PrintWriter; /** * Modifies current brightness based on request */ -abstract class BrightnessModifier { +abstract class BrightnessModifier implements BrightnessStateModifier { private boolean mApplied = false; @@ -37,7 +37,8 @@ abstract class BrightnessModifier { abstract int getModifier(); - void apply(DisplayManagerInternal.DisplayPowerRequest request, + @Override + public void apply(DisplayManagerInternal.DisplayPowerRequest request, DisplayBrightnessState.Builder stateBuilder) { // If low power mode is enabled, scale brightness by screenLowPowerBrightnessFactor // as long as it is above the minimum threshold. @@ -57,8 +58,14 @@ abstract class BrightnessModifier { } } - void dump(PrintWriter pw) { + @Override + public void dump(PrintWriter pw) { pw.println("BrightnessModifier:"); pw.println(" mApplied=" + mApplied); } + + @Override + public void stop() { + // do nothing + } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java new file mode 100644 index 000000000000..441ba8f1a1fc --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java @@ -0,0 +1,45 @@ +/* + * 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.display.brightness.clamper; + +import android.hardware.display.DisplayManagerInternal; + +import com.android.server.display.DisplayBrightnessState; + +import java.io.PrintWriter; + +public interface BrightnessStateModifier { + /** + * Applies the changes to brightness state, by modifying properties of the brightness + * state builder. + * @param request + * @param stateBuilder + */ + void apply(DisplayManagerInternal.DisplayPowerRequest request, + DisplayBrightnessState.Builder stateBuilder); + + /** + * Prints contents of this brightness state modifier + * @param printWriter + */ + void dump(PrintWriter printWriter); + + /** + * Called when stopped. Listeners can be unregistered here. + */ + void stop(); +} diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 35991b356bba..be48eb437dfe 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -37,6 +37,9 @@ public class DisplayManagerFlags { // 'adb shell setprop persist.log.tag.DisplayManagerFlags DEBUG && adb reboot' private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); + private final FlagState mPortInDisplayLayoutFlagState = new FlagState( + Flags.FLAG_ENABLE_PORT_IN_DISPLAY_LAYOUT, + Flags::enablePortInDisplayLayout); private final FlagState mConnectedDisplayManagementFlagState = new FlagState( Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT, @@ -114,6 +117,13 @@ public class DisplayManagerFlags { Flags::refreshRateVotingTelemetry ); + /** + * @return {@code true} if 'port' is allowed in display layout configuration file. + */ + public boolean isPortInDisplayLayoutEnabled() { + return mPortInDisplayLayoutFlagState.isEnabled(); + } + /** Returns whether connected display management is enabled or not. */ public boolean isConnectedDisplayManagementEnabled() { return mConnectedDisplayManagementFlagState.isEnabled(); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index e735282a496e..c9569cbf4b9a 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -3,6 +3,14 @@ package: "com.android.server.display.feature.flags" # Important: Flags must be accessed through DisplayManagerFlags. flag { + name: "enable_port_in_display_layout" + namespace: "display_manager" + description: "Allows refering to displays by port in display layout" + bug: "303058435" + is_fixed_read_only: true +} + +flag { name: "enable_connected_display_management" namespace: "display_manager" description: "Feature flag for Connected Display management" diff --git a/services/core/java/com/android/server/display/layout/Layout.java b/services/core/java/com/android/server/display/layout/Layout.java index 40cb3303adda..8a362f78a9a3 100644 --- a/services/core/java/com/android/server/display/layout/Layout.java +++ b/services/core/java/com/android/server/display/layout/Layout.java @@ -200,13 +200,7 @@ public class Layout { * @return True if the specified address is used in this layout. */ public boolean contains(@NonNull DisplayAddress address) { - final int size = mDisplays.size(); - for (int i = 0; i < size; i++) { - if (address.equals(mDisplays.get(i).getAddress())) { - return true; - } - } - return false; + return getByAddress(address) != null; } /** @@ -237,6 +231,9 @@ public class Layout { if (address.equals(display.getAddress())) { return display; } + if (DisplayAddress.Physical.isPortMatch(address, display.getAddress())) { + return display; + } } return null; } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 0c2eee5aebbe..ad090829a2f6 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -2118,11 +2118,10 @@ public class LockSettingsService extends ILockSettings.Stub { Slogf.d(TAG, "CE storage for user %d is already unlocked", userId); return; } - final UserInfo userInfo = mUserManager.getUserInfo(userId); final String userType = isUserSecure(userId) ? "secured" : "unsecured"; final byte[] secret = sp.deriveFileBasedEncryptionKey(); try { - mStorageManager.unlockCeStorage(userId, userInfo.serialNumber, secret); + mStorageManager.unlockCeStorage(userId, secret); Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId); } catch (RemoteException e) { Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId); diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index 07b333a0fda6..393e7efcce6e 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -198,7 +198,12 @@ public class MediaSession2Record implements MediaSessionRecordImpl { mIsConnected = true; service = mService; } - service.onSessionActiveStateChanged(MediaSession2Record.this); + + // TODO (b/318745416): Add support for FGS in MediaSession2. Passing a + // null playback state means the owning process will not be allowed to + // run in the foreground. + service.onSessionActiveStateChanged(MediaSession2Record.this, + /* playbackState= */ null); } @Override diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index cce66e28a8f9..53f780e4d19e 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -1144,7 +1144,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR mIsActive = active; long token = Binder.clearCallingIdentity(); try { - mService.onSessionActiveStateChanged(MediaSessionRecord.this); + mService.onSessionActiveStateChanged(MediaSessionRecord.this, mPlaybackState); } finally { Binder.restoreCallingIdentity(token); } diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 2affdfcf5c7e..2cd3ab1ddbbb 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -260,7 +260,8 @@ public class MediaSessionService extends SystemService implements Monitor { return mGlobalPrioritySession != null && mGlobalPrioritySession.isActive(); } - void onSessionActiveStateChanged(MediaSessionRecordImpl record) { + void onSessionActiveStateChanged( + MediaSessionRecordImpl record, @Nullable PlaybackState playbackState) { synchronized (mLock) { FullUserRecord user = getFullUserRecordLocked(record.getUserId()); if (user == null) { @@ -287,7 +288,9 @@ public class MediaSessionService extends SystemService implements Monitor { user.mPriorityStack.onSessionActiveStateChanged(record); } setForegroundServiceAllowance( - record, /* allowRunningInForeground= */ record.isActive()); + record, + /* allowRunningInForeground= */ record.isActive() + && (playbackState == null || playbackState.isActive())); mHandler.postSessionsChanged(record); } } @@ -386,7 +389,9 @@ public class MediaSessionService extends SystemService implements Monitor { user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority); if (playbackState != null) { setForegroundServiceAllowance( - record, playbackState.shouldAllowServiceToRunInForeground()); + record, + /* allowRunningInForeground= */ playbackState.isActive() + && record.isActive()); } } } diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index 6b7db2d8d071..85c4ffe6ac67 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -22,6 +22,7 @@ import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; +import static android.media.audio.Flags.focusExclusiveWithRecording; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS; @@ -94,8 +95,6 @@ public final class NotificationAttentionHelper { private static final float DEFAULT_VOLUME = 1.0f; // TODO (b/291899544): remove for release - private static final String POLITE_STRATEGY1 = "rule1"; - private static final String POLITE_STRATEGY2 = "rule2"; private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED = 1; private static final int DEFAULT_NOTIFICATION_COOLDOWN_ENABLED_FOR_WORK = 0; private static final int DEFAULT_NOTIFICATION_COOLDOWN_ALL = 1; @@ -146,7 +145,6 @@ public final class NotificationAttentionHelper { private boolean mNotificationCooldownApplyToAll; private boolean mNotificationCooldownVibrateUnlocked; - private boolean mEnablePoliteNotificationsFeature; private final PolitenessStrategy mStrategy; private int mCurrentWorkProfileId = UserHandle.USER_NULL; @@ -192,9 +190,7 @@ public final class NotificationAttentionHelper { .build(); mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume); - mEnablePoliteNotificationsFeature = Flags.politeNotifications(); - - if (mEnablePoliteNotificationsFeature) { + if (Flags.politeNotifications()) { mStrategy = getPolitenessStrategy(); } else { mStrategy = null; @@ -205,21 +201,23 @@ public final class NotificationAttentionHelper { } private PolitenessStrategy getPolitenessStrategy() { - final String politenessStrategy = mFlagResolver.getStringValue( - NotificationFlags.NOTIF_COOLDOWN_RULE); + if (Flags.crossAppPoliteNotifications()) { + PolitenessStrategy appStrategy = new StrategyPerApp( + mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), + mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2), + mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1), + mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2), + mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET)); - if (POLITE_STRATEGY2.equals(politenessStrategy)) { - return new Strategy2(mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), + return new StrategyGlobal( + mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2), mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1), - mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2)); + mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2), + appStrategy); } else { - if (!POLITE_STRATEGY1.equals(politenessStrategy)) { - Log.w(TAG, "Invalid cooldown strategy: " + politenessStrategy + ". Defaulting to " - + POLITE_STRATEGY1); - } - - return new Strategy1(mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), + return new StrategyPerApp( + mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2), mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1), mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2), @@ -266,7 +264,7 @@ public final class NotificationAttentionHelper { mContext.getContentResolver().registerContentObserver( SettingsObserver.NOTIFICATION_LIGHT_PULSE_URI, false, mSettingsObserver, UserHandle.USER_ALL); - if (mEnablePoliteNotificationsFeature) { + if (Flags.politeNotifications()) { mContext.getContentResolver().registerContentObserver( SettingsObserver.NOTIFICATION_COOLDOWN_ENABLED_URI, false, mSettingsObserver, UserHandle.USER_ALL); @@ -280,7 +278,7 @@ public final class NotificationAttentionHelper { } private void loadUserSettings() { - if (mEnablePoliteNotificationsFeature) { + if (Flags.politeNotifications()) { try { mCurrentWorkProfileId = getManagedProfileId(ActivityManager.getCurrentUser()); @@ -301,11 +299,14 @@ public final class NotificationAttentionHelper { mContext.getContentResolver(), Settings.System.NOTIFICATION_COOLDOWN_ALL, DEFAULT_NOTIFICATION_COOLDOWN_ALL, UserHandle.USER_CURRENT) != 0; - mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser( - mContext.getContentResolver(), - Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, - DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, - UserHandle.USER_CURRENT) != 0; + mStrategy.setApplyCooldownPerPackage(mNotificationCooldownApplyToAll); + if (Flags.vibrateWhileUnlocked()) { + mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, + DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, + UserHandle.USER_CURRENT) != 0; + } } catch (Exception e) { Log.e(TAG, "Failed to read Settings: " + e); } @@ -482,10 +483,10 @@ public final class NotificationAttentionHelper { getPolitenessState(record)); } record.setAudiblyAlerted(buzz || beep); - if (mEnablePoliteNotificationsFeature) { + if (Flags.politeNotifications()) { // Update last alert time if (buzz || beep) { - record.getChannel().setLastNotificationUpdateTimeMs(System.currentTimeMillis()); + mStrategy.setLastNotificationUpdateTimeMs(record, System.currentTimeMillis()); } } return buzzBeepBlinkLoggingCode; @@ -588,37 +589,48 @@ public final class NotificationAttentionHelper { } private boolean playSound(final NotificationRecord record, Uri soundUri) { + final boolean shouldPlay; + if (focusExclusiveWithRecording()) { + // flagged path + shouldPlay = mAudioManager.shouldNotificationSoundPlay(record.getAudioAttributes()); + } else { + // legacy path + // play notifications if there is no user of exclusive audio focus + // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or + // VIBRATE ringer mode) + shouldPlay = !mAudioManager.isAudioFocusExclusive() + && (mAudioManager.getStreamVolume( + AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0); + } + if (!shouldPlay) { + if (DEBUG) Slog.v(TAG, "Not playing sound " + soundUri + " due to focus/volume"); + return false; + } + boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0; - // play notifications if there is no user of exclusive audio focus - // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or - // VIBRATE ringer mode) - if (!mAudioManager.isAudioFocusExclusive() - && (mAudioManager.getStreamVolume( - AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0)) { - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - if (DEBUG) { - Slog.v(TAG, "Playing sound " + soundUri + " with attributes " - + record.getAudioAttributes()); - } - player.playAsync(soundUri, record.getSbn().getUser(), looping, - record.getAudioAttributes(), getSoundVolume(record)); - return true; + final long identity = Binder.clearCallingIdentity(); + try { + final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); + if (player != null) { + if (DEBUG) { + Slog.v(TAG, "Playing sound " + soundUri + " with attributes " + + record.getAudioAttributes()); } - } catch (RemoteException e) { - Log.e(TAG, "Failed playSound: " + e); - } finally { - Binder.restoreCallingIdentity(identity); + player.playAsync(soundUri, record.getSbn().getUser(), looping, + record.getAudioAttributes(), getSoundVolume(record)); + return true; } + } catch (RemoteException e) { + Log.e(TAG, "Failed playSound: " + e); + } finally { + Binder.restoreCallingIdentity(identity); } return false; } private boolean isPoliteNotificationFeatureEnabled(final NotificationRecord record) { // Check feature flag - if (!mEnablePoliteNotificationsFeature) { + if (!Flags.politeNotifications()) { return false; } @@ -1064,9 +1076,13 @@ public final class NotificationAttentionHelper { // Volume for muted state protected final float mVolumeMuted; + protected boolean mApplyPerPackage; + protected final Map<String, Long> mLastUpdatedTimestampByPackage; + public PolitenessStrategy(int timeoutPolite, int timeoutMuted, int volumePolite, int volumeMuted) { mVolumeStates = new HashMap<>(); + mLastUpdatedTimestampByPackage = new HashMap<>(); this.mTimeoutPolite = timeoutPolite; this.mTimeoutMuted = timeoutMuted; @@ -1076,10 +1092,38 @@ public final class NotificationAttentionHelper { abstract void onNotificationPosted(NotificationRecord record); + /** + * Set true if the cooldown strategy should apply per app(package). + * Otherwise apply per conversation channel. + * @param applyPerPackage if the cooldown should be applied per app + */ + void setApplyCooldownPerPackage(boolean applyPerPackage) { + mApplyPerPackage = applyPerPackage; + } + + boolean shouldIgnoreNotification(final NotificationRecord record) { + // Ignore group summaries + return (record.getSbn().isGroup() && record.getSbn().getNotification() + .isGroupSummary()); + } + + /** + * Get the key that determines the grouping for the cooldown behavior. + * + * @param record the notification being posted + * @return the key to group this notification under + */ String getChannelKey(final NotificationRecord record) { - // use conversationId if it's a conversation + // Use conversationId if it's a conversation String channelId = record.getChannel().getConversationId() != null ? record.getChannel().getConversationId() : record.getChannel().getId(); + + // Use only the package name to apply cooldown per app, unless the user explicitly + // changed the channel notification sound => treat separately + if (mApplyPerPackage && !record.getChannel().hasUserSetSound()) { + channelId = ""; + } + return record.getSbn().getNormalizedUserId() + ":" + record.getSbn().getPackageName() + ":" + channelId; } @@ -1121,12 +1165,59 @@ public final class NotificationAttentionHelper { final String key = getChannelKey(record); // reset to default state after user interaction mVolumeStates.put(key, POLITE_STATE_DEFAULT); - record.getChannel().setLastNotificationUpdateTimeMs(0); + setLastNotificationUpdateTimeMs(record, 0); } public final @PolitenessState int getPolitenessState(final NotificationRecord record) { return mVolumeStates.getOrDefault(getChannelKey(record), POLITE_STATE_DEFAULT); } + + void setLastNotificationUpdateTimeMs(final NotificationRecord record, + long timestampMillis) { + record.getChannel().setLastNotificationUpdateTimeMs(timestampMillis); + mLastUpdatedTimestampByPackage.put(record.getSbn().getPackageName(), timestampMillis); + } + + long getLastNotificationUpdateTimeMs(final NotificationRecord record) { + if (record.getChannel().hasUserSetSound() || !mApplyPerPackage) { + return record.getChannel().getLastNotificationUpdateTimeMs(); + } else { + return mLastUpdatedTimestampByPackage.getOrDefault(record.getSbn().getPackageName(), + 0L); + } + } + + @PolitenessState int getNextState(@PolitenessState final int currState, + final long timeSinceLastNotif) { + @PolitenessState int nextState = currState; + switch (currState) { + case POLITE_STATE_DEFAULT: + if (timeSinceLastNotif < mTimeoutPolite) { + nextState = POLITE_STATE_POLITE; + } + break; + case POLITE_STATE_POLITE: + if (timeSinceLastNotif < mTimeoutMuted) { + nextState = POLITE_STATE_MUTED; + } else if (timeSinceLastNotif > mTimeoutPolite) { + nextState = POLITE_STATE_DEFAULT; + } else { + nextState = POLITE_STATE_POLITE; + } + break; + case POLITE_STATE_MUTED: + if (timeSinceLastNotif > mTimeoutMuted) { + nextState = POLITE_STATE_POLITE; + } else { + nextState = POLITE_STATE_MUTED; + } + break; + default: + Log.w(TAG, "getNextState unexpected volume state: " + currState); + break; + } + return nextState; + } } // TODO b/270456865: Only one of the two strategies will be released. @@ -1143,72 +1234,51 @@ public final class NotificationAttentionHelper { * after timeoutMuted. * - Transitions back to the default state after a user interaction with a notification. */ - public static class Strategy1 extends PolitenessStrategy { + private static class StrategyPerApp extends PolitenessStrategy { // Keep track of the number of notifications posted per channel private final Map<String, Integer> mNumPosted; // Reset to default state if number of posted notifications exceed this value when muted private final int mMaxPostedForReset; - public Strategy1(int timeoutPolite, int timeoutMuted, int volumePolite, int volumeMuted, - int maxPosted) { + public StrategyPerApp(int timeoutPolite, int timeoutMuted, int volumePolite, + int volumeMuted, int maxPosted) { super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted); mNumPosted = new HashMap<>(); mMaxPostedForReset = maxPosted; if (DEBUG) { - Log.i(TAG, "Strategy1: " + timeoutPolite + " " + timeoutMuted); + Log.i(TAG, "StrategyPerApp: " + timeoutPolite + " " + timeoutMuted); } } @Override public void onNotificationPosted(final NotificationRecord record) { - long timeSinceLastNotif = System.currentTimeMillis() - - record.getChannel().getLastNotificationUpdateTimeMs(); + if (shouldIgnoreNotification(record)) { + return; + } + + long timeSinceLastNotif = + System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); final String key = getChannelKey(record); - @PolitenessState int volState = getPolitenessState(record); + @PolitenessState final int currState = getPolitenessState(record); + @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); + // Reset to default state if number of posted notifications exceed this value when muted int numPosted = mNumPosted.getOrDefault(key, 0) + 1; mNumPosted.put(key, numPosted); - - switch (volState) { - case POLITE_STATE_DEFAULT: - if (timeSinceLastNotif < mTimeoutPolite) { - volState = POLITE_STATE_POLITE; - } - break; - case POLITE_STATE_POLITE: - if (timeSinceLastNotif < mTimeoutMuted) { - volState = POLITE_STATE_MUTED; - } else if (timeSinceLastNotif > mTimeoutPolite) { - volState = POLITE_STATE_DEFAULT; - } else { - volState = POLITE_STATE_POLITE; - } - break; - case POLITE_STATE_MUTED: - if (timeSinceLastNotif > mTimeoutMuted) { - volState = POLITE_STATE_POLITE; - } else { - volState = POLITE_STATE_MUTED; - } - if (numPosted >= mMaxPostedForReset) { - volState = POLITE_STATE_DEFAULT; - mNumPosted.put(key, 0); - } - break; - default: - Log.w(TAG, "onNotificationPosted unexpected volume state: " + volState); - break; + if (currState == POLITE_STATE_MUTED && numPosted >= mMaxPostedForReset) { + nextState = POLITE_STATE_DEFAULT; + mNumPosted.put(key, 0); } if (DEBUG) { Log.i(TAG, "onNotificationPosted time delta: " + timeSinceLastNotif + " vol state: " - + volState + " key: " + key + " numposted " + numPosted); + + nextState + " key: " + key + " numposted " + numPosted); } - mVolumeStates.put(key, volState); + mVolumeStates.put(key, nextState); } @Override @@ -1219,61 +1289,98 @@ public final class NotificationAttentionHelper { } /** - * Polite notification strategy 2: - * - Transitions from default (loud) => muted state if a notification - * alerts the same channel before timeoutPolite. - * - Transitions from polite => default state if a notification - * alerts the same channel before timeoutMuted. - * - Transitions from muted => default state if a notification alerts after timeoutMuted, - * otherwise transitions to the polite state. - * - Transitions back to the default state after a user interaction with a notification. + * Global (cross-app) strategy. */ - public static class Strategy2 extends PolitenessStrategy { - public Strategy2(int timeoutPolite, int timeoutMuted, int volumePolite, int volumeMuted) { + private static class StrategyGlobal extends PolitenessStrategy { + private static final String COMMON_KEY = "cross_app_common_key"; + + private final PolitenessStrategy mAppStrategy; + private long mLastNotificationTimestamp = 0; + + public StrategyGlobal(int timeoutPolite, int timeoutMuted, int volumePolite, + int volumeMuted, PolitenessStrategy appStrategy) { super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted); + mAppStrategy = appStrategy; + if (DEBUG) { - Log.i(TAG, "Strategy2: " + timeoutPolite + " " + timeoutMuted); + Log.i(TAG, "StrategyGlobal: " + timeoutPolite + " " + timeoutMuted); } } @Override - public void onNotificationPosted(final NotificationRecord record) { - long timeSinceLastNotif = System.currentTimeMillis() - - record.getChannel().getLastNotificationUpdateTimeMs(); + void onNotificationPosted(NotificationRecord record) { + if (shouldIgnoreNotification(record)) { + return; + } + + long timeSinceLastNotif = + System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); final String key = getChannelKey(record); - @PolitenessState int volState = getPolitenessState(record); + @PolitenessState final int currState = getPolitenessState(record); + @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); - switch (volState) { - case POLITE_STATE_DEFAULT: - if (timeSinceLastNotif < mTimeoutPolite) { - volState = POLITE_STATE_MUTED; - } - break; - case POLITE_STATE_POLITE: - if (timeSinceLastNotif > mTimeoutMuted) { - volState = POLITE_STATE_DEFAULT; - } - break; - case POLITE_STATE_MUTED: - if (timeSinceLastNotif > mTimeoutMuted) { - volState = POLITE_STATE_DEFAULT; - } else { - volState = POLITE_STATE_POLITE; - } - break; - default: - Log.w(TAG, "onNotificationPosted unexpected volume state: " + volState); - break; + if (DEBUG) { + Log.i(TAG, "StrategyGlobal onNotificationPosted time delta: " + timeSinceLastNotif + + " vol state: " + nextState + " key: " + key); } - if (DEBUG) { - Log.i(TAG, "onNotificationPosted time delta: " + timeSinceLastNotif + " vol state: " - + volState + " key: " + key); + mVolumeStates.put(key, nextState); + + mAppStrategy.onNotificationPosted(record); + } + + @Override + public float getSoundVolume(final NotificationRecord record) { + final @PolitenessState int globalVolState = getPolitenessState(record); + final @PolitenessState int appVolState = mAppStrategy.getPolitenessState(record); + + // Prioritize the most polite outcome + if (globalVolState > appVolState) { + return super.getSoundVolume(record); + } else { + return mAppStrategy.getSoundVolume(record); + } + } + + @Override + public void onUserInteraction(final NotificationRecord record) { + super.onUserInteraction(record); + mAppStrategy.onUserInteraction(record); + } + + @Override + String getChannelKey(final NotificationRecord record) { + // If the user explicitly changed the channel notification sound: + // handle as a separate channel + if (record.getChannel().hasUserSetSound()) { + return super.getChannelKey(record); + } else { + // Use one global key per user + return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY; } + } - mVolumeStates.put(key, volState); + @Override + public void setLastNotificationUpdateTimeMs(NotificationRecord record, + long timestampMillis) { + super.setLastNotificationUpdateTimeMs(record, timestampMillis); + mLastNotificationTimestamp = timestampMillis; + } + + long getLastNotificationUpdateTimeMs(final NotificationRecord record) { + if (record.getChannel().hasUserSetSound()) { + return super.getLastNotificationUpdateTimeMs(record); + } else { + return mLastNotificationTimestamp; + } + } + + @Override + void setApplyCooldownPerPackage(boolean applyPerPackage) { + super.setApplyCooldownPerPackage(applyPerPackage); + mAppStrategy.setApplyCooldownPerPackage(applyPerPackage); } } @@ -1338,7 +1445,7 @@ public final class NotificationAttentionHelper { updateLightsLocked(); } } - if (mEnablePoliteNotificationsFeature) { + if (Flags.politeNotifications()) { if (NOTIFICATION_COOLDOWN_ENABLED_URI.equals(uri)) { mNotificationCooldownEnabled = Settings.System.getIntForUser( mContext.getContentResolver(), @@ -1363,13 +1470,16 @@ public final class NotificationAttentionHelper { Settings.System.NOTIFICATION_COOLDOWN_ALL, DEFAULT_NOTIFICATION_COOLDOWN_ALL, UserHandle.USER_CURRENT) != 0; + mStrategy.setApplyCooldownPerPackage(mNotificationCooldownApplyToAll); } - if (NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED_URI.equals(uri)) { - mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser( + if (Flags.vibrateWhileUnlocked()) { + if (NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED_URI.equals(uri)) { + mNotificationCooldownVibrateUnlocked = Settings.System.getIntForUser( mContext.getContentResolver(), Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, UserHandle.USER_CURRENT) != 0; + } } } } diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java index 6a46048e80e1..e3880c383632 100644 --- a/services/core/java/com/android/server/notification/NotificationHistoryManager.java +++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java @@ -118,6 +118,10 @@ public class NotificationHistoryManager { } } + public void onUserAdded(@UserIdInt int userId) { + mSettingsObserver.update(null, userId); + } + public void onUserStopped(@UserIdInt int userId) { synchronized (mLock) { mUserUnlockedStates.put(userId, false); @@ -401,9 +405,7 @@ public class NotificationHistoryManager { false, this, UserHandle.USER_ALL); synchronized (mLock) { for (UserInfo userInfo : mUserManager.getUsers()) { - if (!userInfo.isProfile()) { - update(null, userInfo.id); - } + update(null, userInfo.id); } } } @@ -424,10 +426,7 @@ public class NotificationHistoryManager { boolean historyEnabled = Settings.Secure.getIntForUser(resolver, Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId) != 0; - int[] profiles = mUserManager.getProfileIds(userId, true); - for (int profileId : profiles) { - onHistoryEnabledChanged(profileId, historyEnabled); - } + onHistoryEnabledChanged(userId, historyEnabled); } } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 7fbc0852c746..7aa7b7e1bfc1 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -83,6 +83,7 @@ import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.media.audio.Flags.focusExclusiveWithRecording; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; import static android.os.Flags.allowPrivateProfile; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; @@ -2025,6 +2026,8 @@ public class NotificationManagerService extends SystemService { if (!mUserProfiles.isProfileUser(userId)) { allowDefaultApprovedServices(userId); } + mHistoryManager.onUserAdded(userId); + mSettingsObserver.update(null, userId); } } else if (action.equals(Intent.ACTION_USER_REMOVED)) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); @@ -2137,13 +2140,8 @@ public class NotificationManagerService extends SystemService { mPreferencesHelper.updateBubblesEnabled(); } if (uri == null || NOTIFICATION_HISTORY_ENABLED.equals(uri)) { - final IntArray userIds = mUserProfiles.getCurrentProfileIds(); - - for (int i = 0; i < userIds.size(); i++) { - mArchive.updateHistoryEnabled(userIds.get(i), - Settings.Secure.getIntForUser(resolver, - Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, - userIds.get(i)) == 1); + for (UserInfo userInfo : mUm.getUsers()) { + update(uri, userInfo.id); } } if (uri == null || NOTIFICATION_SHOW_MEDIA_ON_QUICK_SETTINGS_URI.equals(uri)) { @@ -2164,6 +2162,17 @@ public class NotificationManagerService extends SystemService { } } } + + public void update(Uri uri, int userId) { + ContentResolver resolver = getContext().getContentResolver(); + if (uri == null || NOTIFICATION_HISTORY_ENABLED.equals(uri)) { + mArchive.updateHistoryEnabled(userId, + Settings.Secure.getIntForUser(resolver, + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, + userId) == 1); + // note: this setting is also handled in NotificationHistoryManager + } + } } private SettingsObserver mSettingsObserver; @@ -9096,27 +9105,40 @@ public class NotificationManagerService extends SystemService { } private boolean playSound(final NotificationRecord record, Uri soundUri) { + final boolean shouldPlay; + if (focusExclusiveWithRecording()) { + // flagged path + shouldPlay = mAudioManager.shouldNotificationSoundPlay(record.getAudioAttributes()); + } else { + // legacy path + // play notifications if there is no user of exclusive audio focus + // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or + // VIBRATE ringer mode) + shouldPlay = !mAudioManager.isAudioFocusExclusive() + && (mAudioManager.getStreamVolume( + AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0); + } + if (!shouldPlay) { + if (DBG) Slog.v(TAG, "Not playing sound " + soundUri + " due to focus/volume"); + return false; + } + boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0; - // play notifications if there is no user of exclusive audio focus - // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or - // VIBRATE ringer mode) - if (!mAudioManager.isAudioFocusExclusive() - && (mAudioManager.getStreamVolume( - AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0)) { - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - if (DBG) Slog.v(TAG, "Playing sound " + soundUri + final long identity = Binder.clearCallingIdentity(); + try { + final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); + if (player != null) { + if (DBG) { + Slog.v(TAG, "Playing sound " + soundUri + " with attributes " + record.getAudioAttributes()); - player.playAsync(soundUri, record.getSbn().getUser(), looping, - record.getAudioAttributes(), 1.0f); - return true; } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); + player.playAsync(soundUri, record.getSbn().getUser(), looping, + record.getAudioAttributes(), 1.0f); + return true; } + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(identity); } return false; } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 64d3a20e9281..1786ac53eeab 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -25,6 +25,7 @@ import static android.service.notification.NotificationListenerService.Ranking.U import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE; import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Flags; import android.app.KeyguardManager; @@ -167,7 +168,7 @@ public final class NotificationRecord { private boolean mPreChannelsNotification = true; private Uri mSound; private VibrationEffect mVibration; - private AudioAttributes mAttributes; + private @NonNull AudioAttributes mAttributes; private NotificationChannel mChannel; private ArrayList<String> mPeopleOverride; private ArrayList<SnoozeCriterion> mSnoozeCriteria; @@ -334,7 +335,7 @@ public final class NotificationRecord { return vibration; } - private AudioAttributes calculateAttributes() { + private @NonNull AudioAttributes calculateAttributes() { final Notification n = getSbn().getNotification(); AudioAttributes attributes = getChannel().getAudioAttributes(); if (attributes == null) { @@ -1003,7 +1004,7 @@ public final class NotificationRecord { } public boolean isAudioAttributesUsage(int usage) { - return mAttributes != null && mAttributes.getUsage() == usage; + return mAttributes.getUsage() == usage; } /** @@ -1172,7 +1173,7 @@ public final class NotificationRecord { return mVibration; } - public AudioAttributes getAudioAttributes() { + public @NonNull AudioAttributes getAudioAttributes() { return mAttributes; } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 3244aff2a4dc..911643b1a634 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -453,6 +453,8 @@ public class ZenModeHelper { ZenRule rule = new ZenRule(); populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true); newConfig.automaticRules.put(rule.id, rule); + maybeReplaceDefaultRule(newConfig, automaticZenRule); + if (setConfigLocked(newConfig, origin, reason, rule.component, true, callingUid)) { return rule.id; } else { @@ -461,6 +463,25 @@ public class ZenModeHelper { } } + private static void maybeReplaceDefaultRule(ZenModeConfig config, AutomaticZenRule addedRule) { + if (!Flags.modesApi()) { + return; + } + if (addedRule.getType() == AutomaticZenRule.TYPE_BEDTIME) { + // Delete a built-in disabled "Sleeping" rule when a BEDTIME rule is added; it may have + // smarter triggers and it will prevent confusion about which one to use. + // Note: we must not verify canManageAutomaticZenRule here, since most likely they + // won't have the same owner (sleeping - system; bedtime - DWB). + ZenRule sleepingRule = config.automaticRules.get( + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); + if (sleepingRule != null + && !sleepingRule.enabled + && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) { + config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); + } + } + } + public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule, @ConfigChangeOrigin int origin, String reason, int callingUid) { ZenModeConfig newConfig; @@ -893,48 +914,206 @@ public class ZenModeHelper { } void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, - @ConfigChangeOrigin int origin, boolean isNew) { - // TODO: b/308671593,b/311406021 - Handle origins more precisely: - // - USER can override anything and updates bitmask of user-modified fields; - // - SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask; - // - APP can only update if not user-modified. - if (rule.enabled != automaticZenRule.isEnabled()) { - rule.snoozing = false; - } - rule.name = automaticZenRule.getName(); - rule.condition = null; - rule.conditionId = automaticZenRule.getConditionId(); - rule.enabled = automaticZenRule.isEnabled(); - rule.modified = automaticZenRule.isModified(); - rule.zenPolicy = automaticZenRule.getZenPolicy(); + @ConfigChangeOrigin int origin, boolean isNew) { if (Flags.modesApi()) { - rule.zenDeviceEffects = fixZenDeviceEffects( - rule.zenDeviceEffects, - automaticZenRule.getDeviceEffects(), - origin); - } - rule.zenMode = NotificationManager.zenModeFromInterruptionFilter( - automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF); - rule.configurationActivity = automaticZenRule.getConfigurationActivity(); - - if (isNew) { - rule.id = ZenModeConfig.newRuleId(); - rule.creationTime = System.currentTimeMillis(); - rule.component = automaticZenRule.getOwner(); - rule.pkg = pkg; - } + // These values can always be edited by the app, so we apply changes immediately. + if (isNew) { + rule.id = ZenModeConfig.newRuleId(); + rule.creationTime = System.currentTimeMillis(); + rule.component = automaticZenRule.getOwner(); + rule.pkg = pkg; + } - if (Flags.modesApi()) { + rule.condition = null; + rule.conditionId = automaticZenRule.getConditionId(); + if (rule.enabled != automaticZenRule.isEnabled()) { + rule.snoozing = false; + } + rule.enabled = automaticZenRule.isEnabled(); + rule.configurationActivity = automaticZenRule.getConfigurationActivity(); rule.allowManualInvocation = automaticZenRule.isManualInvocationAllowed(); - rule.iconResName = drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId()); + rule.iconResName = + drawableResIdToResName(rule.pkg, automaticZenRule.getIconResId()); rule.triggerDescription = automaticZenRule.getTriggerDescription(); rule.type = automaticZenRule.getType(); + // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined. + rule.modified = automaticZenRule.isModified(); + + // Name is treated differently than other values: + // App is allowed to update name if the name was not modified by the user (even if + // other values have been modified). In this way, if the locale of an app changes, + // i18n of the rule name can still occur even if the user has customized the rule + // contents. + String previousName = rule.name; + if (isNew || doesOriginAlwaysUpdateValues(origin) + || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) { + rule.name = automaticZenRule.getName(); + } + + // For the remaining values, rules can always have all values updated if: + // * the rule is newly added, or + // * the request comes from an origin that can always update values, like the user, or + // * the rule has not yet been user modified, and thus can be updated by the app. + boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin) + || rule.canBeUpdatedByApp(); + + // For all other values, if updates are not allowed, we discard the update. + if (!updateValues) { + return; + } + + // Updates the bitmasks if the origin of the change is the user. + boolean updateBitmask = (origin == UPDATE_ORIGIN_USER); + + if (updateBitmask && !TextUtils.equals(previousName, automaticZenRule.getName())) { + rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; + } + int newZenMode = NotificationManager.zenModeFromInterruptionFilter( + automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF); + if (updateBitmask && rule.zenMode != newZenMode) { + rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER; + } + + // Updates the values in the ZenRule itself. + rule.zenMode = newZenMode; + + // Updates the bitmask and values for all policy fields, based on the origin. + rule.zenPolicy = updatePolicy(rule.zenPolicy, automaticZenRule.getZenPolicy(), + updateBitmask); + // Updates the bitmask and values for all device effect fields, based on the origin. + rule.zenDeviceEffects = updateZenDeviceEffects( + rule.zenDeviceEffects, automaticZenRule.getDeviceEffects(), + origin == UPDATE_ORIGIN_APP, updateBitmask); + } else { + if (rule.enabled != automaticZenRule.isEnabled()) { + rule.snoozing = false; + } + rule.name = automaticZenRule.getName(); + rule.condition = null; + rule.conditionId = automaticZenRule.getConditionId(); + 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(); + + if (isNew) { + rule.id = ZenModeConfig.newRuleId(); + rule.creationTime = System.currentTimeMillis(); + rule.component = automaticZenRule.getOwner(); + rule.pkg = pkg; + } } } /** - * Fix {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule. - * + * Returns true when fields can always be updated, based on the provided origin of an AZR + * change. (Note that regardless of origin, fields can always be updated if they're not already + * user modified.) + */ + private static boolean doesOriginAlwaysUpdateValues(@ConfigChangeOrigin int origin) { + return origin == UPDATE_ORIGIN_USER || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI; + } + + /** + * 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}. + */ + @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. + if (newPolicy == null) { + return null; + } + + // 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(); + } + + int userModifiedFields = oldPolicy.getUserModifiedFields(); + if (updateBitmask) { + if (oldPolicy.getPriorityMessageSenders() != newPolicy.getPriorityMessageSenders()) { + userModifiedFields |= ZenPolicy.FIELD_MESSAGES; + } + if (oldPolicy.getPriorityCallSenders() != newPolicy.getPriorityCallSenders()) { + userModifiedFields |= ZenPolicy.FIELD_CALLS; + } + if (oldPolicy.getPriorityConversationSenders() + != newPolicy.getPriorityConversationSenders()) { + userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS; + } + if (oldPolicy.getAllowedChannels() != newPolicy.getAllowedChannels()) { + userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS; + } + if (oldPolicy.getPriorityCategoryReminders() + != newPolicy.getPriorityCategoryReminders()) { + userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS; + } + if (oldPolicy.getPriorityCategoryEvents() != newPolicy.getPriorityCategoryEvents()) { + userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS; + } + if (oldPolicy.getPriorityCategoryRepeatCallers() + != newPolicy.getPriorityCategoryRepeatCallers()) { + userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS; + } + if (oldPolicy.getPriorityCategoryAlarms() != newPolicy.getPriorityCategoryAlarms()) { + userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_ALARMS; + } + if (oldPolicy.getPriorityCategoryMedia() != newPolicy.getPriorityCategoryMedia()) { + userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_MEDIA; + } + if (oldPolicy.getPriorityCategorySystem() != newPolicy.getPriorityCategorySystem()) { + userModifiedFields |= ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM; + } + // Visual effects + if (oldPolicy.getVisualEffectFullScreenIntent() + != newPolicy.getVisualEffectFullScreenIntent()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT; + } + if (oldPolicy.getVisualEffectLights() != newPolicy.getVisualEffectLights()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS; + } + if (oldPolicy.getVisualEffectPeek() != newPolicy.getVisualEffectPeek()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_PEEK; + } + if (oldPolicy.getVisualEffectStatusBar() != newPolicy.getVisualEffectStatusBar()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_STATUS_BAR; + } + if (oldPolicy.getVisualEffectBadge() != newPolicy.getVisualEffectBadge()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_BADGE; + } + if (oldPolicy.getVisualEffectAmbient() != newPolicy.getVisualEffectAmbient()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT; + } + if (oldPolicy.getVisualEffectNotificationList() + != newPolicy.getVisualEffectNotificationList()) { + userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_NOTIFICATION_LIST; + } + } + + // 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 @@ -942,38 +1121,85 @@ public class ZenModeHelper { * </ul> */ @Nullable - private static ZenDeviceEffects fixZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects, - @Nullable ZenDeviceEffects newEffects, @ConfigChangeOrigin int origin) { - // TODO: b/308671593,b/311406021 - Handle origins more precisely: - // - USER can override anything and updates bitmask of user-modified fields; - // - SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask; - // - APP can only update if not user-modified. - if (origin != UPDATE_ORIGIN_APP) { - return newEffects; - } - + private static ZenDeviceEffects updateZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects, + @Nullable ZenDeviceEffects newEffects, + boolean isFromApp, + boolean updateBitmask) { if (newEffects == null) { return null; } - if (oldEffects != null) { - return new ZenDeviceEffects.Builder(newEffects) - .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness()) - .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake()) - .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake()) - .setShouldDisableTouch(oldEffects.shouldDisableTouch()) - .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage()) - .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze()) - .build(); - } else { - return new ZenDeviceEffects.Builder(newEffects) - .setShouldDisableAutoBrightness(false) - .setShouldDisableTapToWake(false) - .setShouldDisableTiltToWake(false) - .setShouldDisableTouch(false) - .setShouldMinimizeRadioUsage(false) - .setShouldMaximizeDoze(false) - .build(); + + // Since newEffects is not null, we want to adopt all the new provided device effects. + ZenDeviceEffects.Builder builder = new ZenDeviceEffects.Builder(newEffects); + + 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(); + } + } + + // 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(); + } + + int userModifiedFields = oldEffects.getUserModifiedFields(); + if (updateBitmask) { + if (oldEffects.shouldDisplayGrayscale() != newEffects.shouldDisplayGrayscale()) { + userModifiedFields |= ZenDeviceEffects.FIELD_GRAYSCALE; + } + if (oldEffects.shouldSuppressAmbientDisplay() + != newEffects.shouldSuppressAmbientDisplay()) { + userModifiedFields |= ZenDeviceEffects.FIELD_SUPPRESS_AMBIENT_DISPLAY; + } + if (oldEffects.shouldDimWallpaper() != newEffects.shouldDimWallpaper()) { + userModifiedFields |= ZenDeviceEffects.FIELD_DIM_WALLPAPER; + } + if (oldEffects.shouldUseNightMode() != newEffects.shouldUseNightMode()) { + userModifiedFields |= ZenDeviceEffects.FIELD_NIGHT_MODE; + } + if (oldEffects.shouldDisableAutoBrightness() + != newEffects.shouldDisableAutoBrightness()) { + userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_AUTO_BRIGHTNESS; + } + if (oldEffects.shouldDisableTapToWake() != newEffects.shouldDisableTapToWake()) { + userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TAP_TO_WAKE; + } + if (oldEffects.shouldDisableTiltToWake() != newEffects.shouldDisableTiltToWake()) { + userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TILT_TO_WAKE; + } + if (oldEffects.shouldDisableTouch() != newEffects.shouldDisableTouch()) { + userModifiedFields |= ZenDeviceEffects.FIELD_DISABLE_TOUCH; + } + if (oldEffects.shouldMinimizeRadioUsage() != newEffects.shouldMinimizeRadioUsage()) { + userModifiedFields |= ZenDeviceEffects.FIELD_MINIMIZE_RADIO_USAGE; + } + if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) { + userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE; + } } + + // 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) { @@ -992,6 +1218,7 @@ public class ZenModeHelper { .setOwner(rule.component) .setConfigurationActivity(rule.configurationActivity) .setTriggerDescription(rule.triggerDescription) + .setUserModifiedFields(rule.userModifiedFields) .build(); } else { azr = new AutomaticZenRule(rule.name, rule.component, @@ -1171,6 +1398,10 @@ public class ZenModeHelper { // reset zen automatic rules to default on restore or upgrade if: // - doesn't already have default rules and // - all previous automatic rules were disabled + // + // Note: we don't need to check to avoid restoring the Sleeping rule if there is a + // TYPE_BEDTIME rule because the config is from an old version and thus by + // definition cannot have a rule with TYPE_BEDTIME (or any other type). config.automaticRules = new ArrayMap<>(); for (ZenRule rule : mDefaultConfig.automaticRules.values()) { config.automaticRules.put(rule.id, rule); @@ -2023,6 +2254,7 @@ public class ZenModeHelper { if (resId == 0) { return null; } + Objects.requireNonNull(packageName); try { final Resources res = mPm.getResourcesForApplication(packageName); return res.getResourceName(resId); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 49db7fc280d9..47967dbcaba2 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -56,4 +56,11 @@ flag { namespace: "systemui" description: "When this flag is on, NMS will no longer call removeMessage() and hasCallbacks() on Handler" bug: "311051285" +} + +flag { + name: "notification_test" + namespace: "systemui" + description: "Timing test, no functionality" + bug: "316931130" }
\ No newline at end of file diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index f6e7ef3d50e9..9ce3cb3abe4a 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -41,14 +41,13 @@ 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; @@ -130,18 +129,21 @@ public final class NativeTombstoneManager { return; } - String processName = "UNKNOWN"; final boolean isProtoFile = filename.endsWith(".pb"); - File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb"); + if (!isProtoFile) { + return; + } - Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile); + Optional<ParsedTombstone> parsedTombstone = handleProtoTombstone(path, true); if (parsedTombstone.isPresent()) { - processName = parsedTombstone.get().getProcessName(); + BootReceiver.addTombstoneToDropBox( + mContext, path, parsedTombstone.get().getTombstone(), + parsedTombstone.get().getProcessName(), mTmpFileLock); } - BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock); } - private Optional<TombstoneFile> handleProtoTombstone(File path, boolean addToList) { + private Optional<ParsedTombstone> handleProtoTombstone( + File path, boolean addToList) { final String filename = path.getName(); if (!filename.endsWith(".pb")) { Slog.w(TAG, "unexpected tombstone name: " + path); @@ -171,7 +173,7 @@ public final class NativeTombstoneManager { return Optional.empty(); } - final Optional<TombstoneFile> parsedTombstone = TombstoneFile.parse(pfd); + final Optional<ParsedTombstone> parsedTombstone = TombstoneFile.parse(pfd); if (!parsedTombstone.isPresent()) { IoUtils.closeQuietly(pfd); return Optional.empty(); @@ -184,7 +186,7 @@ public final class NativeTombstoneManager { previous.dispose(); } - mTombstones.put(number, parsedTombstone.get()); + mTombstones.put(number, parsedTombstone.get().getTombstoneFile()); } } @@ -332,6 +334,27 @@ 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; @@ -414,67 +437,21 @@ public final class NativeTombstoneManager { } } - 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; + static Optional<ParsedTombstone> parse(ParcelFileDescriptor pfd) { + Tombstone tombstoneProto; + try (FileInputStream is = new FileInputStream(pfd.getFileDescriptor())) { + final byte[] tombstoneBytes = is.readAllBytes(); - 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) { + tombstoneProto = Tombstone.parseFrom( + CodedInputStream.newInstance(tombstoneBytes)); + } catch (IOException 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(); @@ -491,6 +468,7 @@ 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(); @@ -502,11 +480,30 @@ public final class NativeTombstoneManager { result.mAppId = appId; result.mPid = pid; result.mUid = uid; - result.mProcessName = processName == null ? "" : processName; + result.mProcessName = getCmdLineProcessName(tombstoneProto); result.mTimestampMs = timestampMs; - result.mCrashReason = crashReason; + result.mCrashReason = getCrashReason(tombstoneProto); - return Optional.of(result); + 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 ""; } public IParcelFileDescriptorRetriever getPfdRetriever() { diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 992d8eb8b1bb..dd9541e5dda1 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -175,6 +175,7 @@ import com.android.server.SystemConfig; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; +import com.android.server.criticalevents.CriticalEventLog; import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; @@ -957,6 +958,7 @@ final class InstallPackageHelper { final Set<String> scannedPackages = new ArraySet<>(requests.size()); final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size()); final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size()); + CriticalEventLog.getInstance().logInstallPackagesStarted(); boolean success = false; try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI"); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 376b06105b8c..a4af5e71000c 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -27,6 +27,7 @@ import static android.content.pm.ArchivedActivityInfo.drawableToBitmap; import static android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_STATUS; import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION; import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; +import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageManager.DELETE_ARCHIVE; import static android.content.pm.PackageManager.DELETE_KEEP_DATA; import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; @@ -100,6 +101,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; /** @@ -210,7 +212,6 @@ public class PackageArchiver { return; } - // TODO(b/278553670) Add special strings for the delete dialog mPm.mInstallerService.uninstall( new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), @@ -264,7 +265,7 @@ public class PackageArchiver { try { // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options. requestUnarchive(packageName, callerPackageName, - getOrCreateUnarchiveIntentSender(userId, packageName), + getOrCreateLauncherListener(userId, packageName), UserHandle.of(userId), false /* showUnarchivalConfirmation= */); } catch (Throwable t) { @@ -329,7 +330,7 @@ public class PackageArchiver { return true; } - private IntentSender getOrCreateUnarchiveIntentSender(int userId, String packageName) { + private IntentSender getOrCreateLauncherListener(int userId, String packageName) { Pair<Integer, String> key = Pair.create(userId, packageName); synchronized (mLauncherIntentSenders) { IntentSender intentSender = mLauncherIntentSenders.get(key); @@ -515,7 +516,6 @@ public class PackageArchiver { /** * Returns true if the app is archivable. */ - // TODO(b/299299569) Exclude system apps public boolean isAppArchivable(@NonNull String packageName, @NonNull UserHandle user) { Objects.requireNonNull(packageName); Objects.requireNonNull(user); @@ -685,15 +685,14 @@ public class PackageArchiver { PackageInstaller.SessionParams.MODE_FULL_INSTALL); sessionParams.setAppPackageName(packageName); sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT; - sessionParams.unarchiveIntentSender = statusReceiver; int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId); // Handles case of repeated unarchival calls for the same package. - // TODO(b/316881759) Allow attaching multiple intentSenders to one session. int existingSessionId = mPm.mInstallerService.getExistingDraftSessionId(installerUid, sessionParams, userId); if (existingSessionId != PackageInstaller.SessionInfo.INVALID_ID) { + attachListenerToSession(statusReceiver, existingSessionId, userId); return existingSessionId; } @@ -702,12 +701,34 @@ public class PackageArchiver { installerPackage, mContext.getAttributionTag(), installerUid, userId); + attachListenerToSession(statusReceiver, sessionId, userId); + // TODO(b/297358628) Also cleanup sessions upon device restart. mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), getUnarchiveForegroundTimeout()); return sessionId; } + private void attachListenerToSession(IntentSender statusReceiver, int existingSessionId, + int userId) { + PackageInstallerSession session = mPm.mInstallerService.getSession(existingSessionId); + int status = session.getUnarchivalStatus(); + // Here we handle a race condition that might happen when an installer reports UNARCHIVAL_OK + // but hasn't created a session yet. Without this the listener would never receive a success + // response. + if (status == UNARCHIVAL_OK) { + notifyUnarchivalListener(UNARCHIVAL_OK, session.getInstallerPackageName(), + session.params.appPackageName, /* requiredStorageBytes= */ 0, + /* userActionIntent= */ null, Set.of(statusReceiver), userId); + return; + } else if (status != UNARCHIVAL_STATUS_UNSET) { + throw new IllegalStateException(TextUtils.formatSimple("Session %s has unarchive status" + + "%s but is still active.", session.sessionId, status)); + } + + session.registerUnarchivalListener(statusReceiver); + } + /** * Returns the icon of an archived app. This is the icon of the main activity of the app. * @@ -883,13 +904,7 @@ public class PackageArchiver { void notifyUnarchivalListener(int status, String installerPackageName, String appPackageName, long requiredStorageBytes, @Nullable PendingIntent userActionIntent, - @Nullable IntentSender unarchiveIntentSender, int userId) { - if (unarchiveIntentSender == null) { - // Maybe this can happen if the installer calls reportUnarchivalStatus twice in quick - // succession. - return; - } - + Set<IntentSender> unarchiveIntentSenders, int userId) { final Intent broadcastIntent = new Intent(); broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, appPackageName); broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status); @@ -909,15 +924,16 @@ public class PackageArchiver { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); - try { - unarchiveIntentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null, - /* handler= */ null, /* requiredPermission= */ null, - options.toBundle()); - } catch (IntentSender.SendIntentException e) { - Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); - } finally { - synchronized (mLauncherIntentSenders) { - mLauncherIntentSenders.remove(Pair.create(userId, appPackageName)); + for (IntentSender intentSender : unarchiveIntentSenders) { + try { + intentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null, + /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); + } catch (IntentSender.SendIntentException e) { + Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); + } finally { + synchronized (mLauncherIntentSenders) { + mLauncherIntentSenders.remove(Pair.create(userId, appPackageName)); + } } } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 0a23dfb80f3b..a9118d46b4ba 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1759,26 +1759,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements binderUid, unarchiveId)); } - IntentSender unarchiveIntentSender = session.params.unarchiveIntentSender; - if (unarchiveIntentSender == null) { - throw new IllegalStateException( - TextUtils.formatSimple( - "Unarchival status for ID %s has already been set or a " - + "session has been created for it already by the " - + "caller.", - unarchiveId)); - } - - // Execute expensive calls outside the sync block. - mPm.mHandler.post( - () -> mPackageArchiver.notifyUnarchivalListener(status, - session.getInstallerPackageName(), - session.params.appPackageName, requiredStorageBytes, userActionIntent, - unarchiveIntentSender, userId)); - session.params.unarchiveIntentSender = null; - if (status != UNARCHIVAL_OK) { - Binder.withCleanCallingIdentity(session::abandon); - } + session.reportUnarchivalStatus(unarchiveId, status, requiredStorageBytes, + userActionIntent); } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 4adb60c34c52..117d03fd059b 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -22,6 +22,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_UPDAT import static android.content.pm.DataLoaderType.INCREMENTAL; import static android.content.pm.DataLoaderType.STREAMING; import static android.content.pm.PackageInstaller.LOCATION_DATA_APP; +import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; +import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; @@ -65,6 +67,7 @@ import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.Notification; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; @@ -97,6 +100,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.PreapprovalDetails; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; +import android.content.pm.PackageInstaller.UnarchivalStatus; import android.content.pm.PackageInstaller.UserActionReason; import android.content.pm.PackageManager; import android.content.pm.PackageManager.PackageInfoFlags; @@ -771,6 +775,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final List<String> mResolvedInstructionSets = new ArrayList<>(); @GuardedBy("mLock") private final List<String> mResolvedNativeLibPaths = new ArrayList<>(); + + @GuardedBy("mLock") + private final Set<IntentSender> mUnarchivalListeners = new ArraySet<>(); + @GuardedBy("mLock") private File mInheritedFilesBase; @GuardedBy("mLock") @@ -796,6 +804,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private int mValidatedTargetSdk = INVALID_TARGET_SDK_VERSION; + @UnarchivalStatus + private int mUnarchivalStatus = UNARCHIVAL_STATUS_UNSET; + private static final FileFilter sAddedApkFilter = new FileFilter() { @Override public boolean accept(File file) { @@ -5088,6 +5099,44 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + void registerUnarchivalListener(IntentSender intentSender) { + synchronized (mLock) { + this.mUnarchivalListeners.add(intentSender); + } + } + + Set<IntentSender> getUnarchivalListeners() { + synchronized (mLock) { + return new ArraySet<>(mUnarchivalListeners); + } + } + + void reportUnarchivalStatus(@UnarchivalStatus int status, int unarchiveId, + long requiredStorageBytes, PendingIntent userActionIntent) { + if (getUnarchivalStatus() != UNARCHIVAL_STATUS_UNSET) { + throw new IllegalStateException( + TextUtils.formatSimple( + "Unarchival status for ID %s has already been set or a session has " + + "been created for it already by the caller.", + unarchiveId)); + } + mUnarchivalStatus = status; + + // Execute expensive calls outside the sync block. + mPm.mHandler.post( + () -> mPm.mInstallerService.mPackageArchiver.notifyUnarchivalListener(status, + getInstallerPackageName(), params.appPackageName, requiredStorageBytes, + userActionIntent, getUnarchivalListeners(), userId)); + if (status != UNARCHIVAL_OK) { + Binder.withCleanCallingIdentity(this::abandon); + } + } + + @UnarchivalStatus + int getUnarchivalStatus() { + return this.mUnarchivalStatus; + } + /** * Free up storage used by this session and its children. * Must not be called on a child session. diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java index d05e4c69427e..cf5de897cf5d 100644 --- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java +++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java @@ -48,6 +48,7 @@ import java.util.function.BiFunction; class PackageMonitorCallbackHelper { private static final boolean DEBUG = false; + private static final String TAG = "PackageMonitorCallbackHelper"; @NonNull private final Object mLock = new Object(); @@ -243,25 +244,33 @@ class PackageMonitorCallbackHelper { return; } int registerUid = registerUser.getUid(); + if (allowUids != null && registerUid != Process.SYSTEM_UID + && !ArrayUtils.contains(allowUids, registerUid)) { + if (DEBUG) { + Slog.w(TAG, "Skip invoke PackageMonitorCallback for " + intent.getAction() + + ", uid " + registerUid); + } + return; + } + Intent newIntent = intent; if (filterExtrasFunction != null) { final Bundle extras = intent.getExtras(); if (extras != null) { final Bundle filteredExtras = filterExtrasFunction.apply(registerUid, extras); - if (filteredExtras != null) { - intent.replaceExtras(filteredExtras); + if (filteredExtras == null) { + // caller is unable to access this intent + if (DEBUG) { + Slog.w(TAG, + "Skip invoke PackageMonitorCallback for " + intent.getAction() + + " because null filteredExtras"); + } + return; } + newIntent = new Intent(newIntent); + newIntent.replaceExtras(filteredExtras); } } - if (allowUids != null && registerUid != Process.SYSTEM_UID - && !ArrayUtils.contains(allowUids, registerUid)) { - if (DEBUG) { - Slog.w("PackageMonitorCallbackHelper", - "Skip invoke PackageMonitorCallback for " + intent.getAction() - + ", uid " + registerUid); - } - return; - } - invokeCallback(callback, intent); + invokeCallback(callback, newIntent); })); } diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java index 7d87d1b27da0..cef3244c9068 100644 --- a/services/core/java/com/android/server/pm/StorageEventHelper.java +++ b/services/core/java/com/android/server/pm/StorageEventHelper.java @@ -193,7 +193,7 @@ public final class StorageEventHelper extends StorageEventListener { } try { - sm.prepareUserStorage(volumeUuid, user.id, user.serialNumber, flags); + sm.prepareUserStorage(volumeUuid, user.id, flags); synchronized (mPm.mInstallLock) { appDataHelper.reconcileAppsDataLI(volumeUuid, user.id, flags, true /* migrateAppData */); diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java index 8adb5661ad1d..4c42c2dd0850 100644 --- a/services/core/java/com/android/server/pm/UserDataPreparer.java +++ b/services/core/java/com/android/server/pm/UserDataPreparer.java @@ -92,7 +92,7 @@ class UserDataPreparer { volumeUuid, userId, flags, isNewUser); try { // Prepare CE and/or DE storage. - storage.prepareUserStorage(volumeUuid, userId, userSerial, flags); + storage.prepareUserStorage(volumeUuid, userId, flags); // Ensure that the data directories of a removed user with the same ID are not being // reused. New users must get fresh data directories, to avoid leaking data. diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 75b453184db8..49af4fedb643 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -5136,7 +5136,7 @@ public class UserManagerService extends IUserManager.Stub { t.traceBegin("createUserStorageKeys"); final StorageManager storage = mContext.getSystemService(StorageManager.class); - storage.createUserStorageKeys(userId, userInfo.serialNumber, userInfo.isEphemeral()); + storage.createUserStorageKeys(userId, userInfo.isEphemeral()); t.traceEnd(); // Only prepare DE storage here. CE storage will be prepared later, when the user is diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java index 752eb5315cc1..17c901e56407 100644 --- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java +++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java @@ -254,6 +254,14 @@ public class DomainVerificationProxyV1 implements DomainVerificationProxy { String packageName = verifications.valueAt(index).second; AndroidPackage pkg = mConnection.getPackage(packageName); + if (pkg == null) { + if (DEBUG_BROADCASTS) { + Slog.d(TAG, + "Skip sendBroadcasts because null AndroidPackage for " + packageName); + } + continue; + } + String hostsString = buildHostsString(pkg); Intent intent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION) diff --git a/services/core/java/com/android/server/policy/Android.bp b/services/core/java/com/android/server/policy/Android.bp new file mode 100644 index 000000000000..fa55bf0a30e5 --- /dev/null +++ b/services/core/java/com/android/server/policy/Android.bp @@ -0,0 +1,10 @@ +aconfig_declarations { + name: "policy_flags", + package: "com.android.server.policy", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "policy_flags_lib", + aconfig_declarations: "policy_flags", +} diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f651dbf591d1..bf669fba82ce 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1301,7 +1301,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.THEATER_MODE_ON, 0); if (!interactive) { - wakeUpFromWakeKey(eventTime, KEYCODE_POWER); + wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false); } } else { Slog.i(TAG, "Toggling theater mode on."); @@ -1317,7 +1317,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { case MULTI_PRESS_POWER_BRIGHTNESS_BOOST: Slog.i(TAG, "Starting brightness boost."); if (!interactive) { - wakeUpFromWakeKey(eventTime, KEYCODE_POWER); + wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false); } mPowerManager.boostScreenBrightness(eventTime); break; @@ -5185,7 +5185,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action, long whenNanos, int policyFlags) { if ((policyFlags & FLAG_WAKE) != 0) { - if (mWindowWakeUpPolicy.wakeUpFromMotion(whenNanos / 1000000)) { + if (mWindowWakeUpPolicy.wakeUpFromMotion( + whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) { // Woke up. Pass motion events to user. return ACTION_PASS_TO_USER; } @@ -5199,7 +5200,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // there will be no dream to intercept the touch and wake into ambient. The device should // wake up in this case. if (isTheaterModeEnabled() && (policyFlags & FLAG_WAKE) != 0) { - if (mWindowWakeUpPolicy.wakeUpFromMotion(whenNanos / 1000000)) { + if (mWindowWakeUpPolicy.wakeUpFromMotion( + whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) { // Woke up. Pass motion events to user. return ACTION_PASS_TO_USER; } @@ -5534,11 +5536,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private void wakeUpFromWakeKey(KeyEvent event) { - wakeUpFromWakeKey(event.getEventTime(), event.getKeyCode()); + wakeUpFromWakeKey( + event.getEventTime(), + event.getKeyCode(), + event.getAction() == KeyEvent.ACTION_DOWN); } - private void wakeUpFromWakeKey(long eventTime, int keyCode) { - if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode)) { + private void wakeUpFromWakeKey(long eventTime, int keyCode, boolean isDown) { + if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode, isDown)) { final boolean keyCanLaunchHome = keyCode == KEYCODE_HOME || keyCode == KEYCODE_POWER; // Start HOME with "reason" extra if sleeping for more than mWakeUpToLastStateTimeout if (shouldWakeUpWithHomeIntent() && keyCanLaunchHome) { diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java index 392d0d4fdb52..a790950e7b9a 100644 --- a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java +++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java @@ -24,6 +24,9 @@ import static android.os.PowerManager.WAKE_REASON_WAKE_KEY; import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION; import static android.view.KeyEvent.KEYCODE_POWER; +import static com.android.server.policy.Flags.supportInputWakeupDelegate; + +import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.os.PowerManager; @@ -31,7 +34,11 @@ import android.os.PowerManager.WakeReason; import android.os.SystemClock; import android.provider.Settings; import android.util.Slog; +import android.view.KeyEvent; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; +import com.android.server.LocalServices; /** Policy controlling the decision and execution of window-related wake ups. */ class WindowWakeUpPolicy { @@ -41,18 +48,27 @@ class WindowWakeUpPolicy { private final Context mContext; private final PowerManager mPowerManager; + private final Clock mClock; private final boolean mAllowTheaterModeWakeFromKey; private final boolean mAllowTheaterModeWakeFromPowerKey; private final boolean mAllowTheaterModeWakeFromMotion; - private final boolean mAllowTheaterModeWakeFromMotionWhenNotDreaming; private final boolean mAllowTheaterModeWakeFromCameraLens; private final boolean mAllowTheaterModeWakeFromLidSwitch; private final boolean mAllowTheaterModeWakeFromWakeGesture; + // The policy will handle input-based wake ups if this delegate is null. + @Nullable private WindowWakeUpPolicyInternal.InputWakeUpDelegate mInputWakeUpDelegate; + WindowWakeUpPolicy(Context context) { + this(context, Clock.SYSTEM_CLOCK); + } + + @VisibleForTesting + WindowWakeUpPolicy(Context context, Clock clock) { mContext = context; mPowerManager = context.getSystemService(PowerManager.class); + mClock = clock; final Resources res = context.getResources(); mAllowTheaterModeWakeFromKey = res.getBoolean( @@ -62,14 +78,26 @@ class WindowWakeUpPolicy { com.android.internal.R.bool.config_allowTheaterModeWakeFromPowerKey); mAllowTheaterModeWakeFromMotion = res.getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromMotion); - mAllowTheaterModeWakeFromMotionWhenNotDreaming = res.getBoolean( - com.android.internal.R.bool.config_allowTheaterModeWakeFromMotionWhenNotDreaming); mAllowTheaterModeWakeFromCameraLens = res.getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromCameraLens); mAllowTheaterModeWakeFromLidSwitch = res.getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch); mAllowTheaterModeWakeFromWakeGesture = res.getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture); + if (supportInputWakeupDelegate()) { + LocalServices.addService(WindowWakeUpPolicyInternal.class, new LocalService()); + } + } + + private final class LocalService implements WindowWakeUpPolicyInternal { + @Override + public void setInputWakeUpDelegate(@Nullable InputWakeUpDelegate delegate) { + if (!supportInputWakeupDelegate()) { + Slog.w(TAG, "Input wake up delegates not supported."); + return; + } + mInputWakeUpDelegate = delegate; + } } /** @@ -77,31 +105,49 @@ class WindowWakeUpPolicy { * * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}. * @param keyCode the {@link android.view.KeyEvent} key code of the key event. + * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}. * @return {@code true} if the policy allows the requested wake up and the request has been * executed; {@code false} otherwise. */ - boolean wakeUpFromKey(long eventTime, int keyCode) { + boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown) { final boolean wakeAllowedDuringTheaterMode = keyCode == KEYCODE_POWER ? mAllowTheaterModeWakeFromPowerKey : mAllowTheaterModeWakeFromKey; - return wakeUp( + if (!canWakeUp(wakeAllowedDuringTheaterMode)) { + if (DEBUG) Slog.d(TAG, "Unable to wake up from " + KeyEvent.keyCodeToString(keyCode)); + return false; + } + if (mInputWakeUpDelegate != null + && mInputWakeUpDelegate.wakeUpFromKey(eventTime, keyCode, isDown)) { + return true; + } + wakeUp( eventTime, - wakeAllowedDuringTheaterMode, keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY, keyCode == KEYCODE_POWER ? "POWER" : "KEY"); + return true; } /** * Wakes up from a motion event. * * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}. + * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}. * @return {@code true} if the policy allows the requested wake up and the request has been * executed; {@code false} otherwise. */ - boolean wakeUpFromMotion(long eventTime) { - return wakeUp( - eventTime, mAllowTheaterModeWakeFromMotion, WAKE_REASON_WAKE_MOTION, "MOTION"); + boolean wakeUpFromMotion(long eventTime, int source, boolean isDown) { + if (!canWakeUp(mAllowTheaterModeWakeFromMotion)) { + if (DEBUG) Slog.d(TAG, "Unable to wake up from motion."); + return false; + } + if (mInputWakeUpDelegate != null + && mInputWakeUpDelegate.wakeUpFromMotion(eventTime, source, isDown)) { + return true; + } + wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION"); + return true; } /** @@ -112,11 +158,12 @@ class WindowWakeUpPolicy { * executed; {@code false} otherwise. */ boolean wakeUpFromCameraCover(long eventTime) { - return wakeUp( - eventTime, - mAllowTheaterModeWakeFromCameraLens, - WAKE_REASON_CAMERA_LAUNCH, - "CAMERA_COVER"); + if (!canWakeUp(mAllowTheaterModeWakeFromCameraLens)) { + if (DEBUG) Slog.d(TAG, "Unable to wake up from camera cover."); + return false; + } + wakeUp(eventTime, WAKE_REASON_CAMERA_LAUNCH, "CAMERA_COVER"); + return true; } /** @@ -126,11 +173,12 @@ class WindowWakeUpPolicy { * executed; {@code false} otherwise. */ boolean wakeUpFromLid() { - return wakeUp( - SystemClock.uptimeMillis(), - mAllowTheaterModeWakeFromLidSwitch, - WAKE_REASON_LID, - "LID"); + if (!canWakeUp(mAllowTheaterModeWakeFromLidSwitch)) { + if (DEBUG) Slog.d(TAG, "Unable to wake up from lid."); + return false; + } + wakeUp(mClock.uptimeMillis(), WAKE_REASON_LID, "LID"); + return true; } /** @@ -140,11 +188,12 @@ class WindowWakeUpPolicy { * executed; {@code false} otherwise. */ boolean wakeUpFromPowerKeyCameraGesture() { - return wakeUp( - SystemClock.uptimeMillis(), - mAllowTheaterModeWakeFromPowerKey, - WAKE_REASON_CAMERA_LAUNCH, - "CAMERA_GESTURE_PREVENT_LOCK"); + if (!canWakeUp(mAllowTheaterModeWakeFromPowerKey)) { + if (DEBUG) Slog.d(TAG, "Unable to wake up from power key camera gesture."); + return false; + } + wakeUp(mClock.uptimeMillis(), WAKE_REASON_CAMERA_LAUNCH, "CAMERA_GESTURE_PREVENT_LOCK"); + return true; } /** @@ -154,23 +203,23 @@ class WindowWakeUpPolicy { * executed; {@code false} otherwise. */ boolean wakeUpFromWakeGesture() { - return wakeUp( - SystemClock.uptimeMillis(), - mAllowTheaterModeWakeFromWakeGesture, - WAKE_REASON_GESTURE, - "GESTURE"); + if (!canWakeUp(mAllowTheaterModeWakeFromWakeGesture)) { + if (DEBUG) Slog.d(TAG, "Unable to wake up from gesture."); + return false; + } + wakeUp(mClock.uptimeMillis(), WAKE_REASON_GESTURE, "GESTURE"); + return true; } - private boolean wakeUp( - long wakeTime, boolean wakeInTheaterMode, @WakeReason int reason, String details) { + private boolean canWakeUp(boolean wakeInTheaterMode) { final boolean isTheaterModeEnabled = Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.THEATER_MODE_ON, 0) == 1; - if (!wakeInTheaterMode && isTheaterModeEnabled) { - if (DEBUG) Slog.d(TAG, "Unable to wake up from " + details); - return false; - } + return wakeInTheaterMode || !isTheaterModeEnabled; + } + + /** Wakes up {@link PowerManager}. */ + private void wakeUp(long wakeTime, @WakeReason int reason, String details) { mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details); - return true; } } diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicyInternal.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicyInternal.java new file mode 100644 index 000000000000..66a003577e9a --- /dev/null +++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicyInternal.java @@ -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.server.policy; + +import android.annotation.Nullable; +import android.os.SystemClock; + +import com.android.internal.annotations.Keep; +import com.android.server.LocalServices; + +/** Policy controlling the decision and execution of window-related wake ups. */ +@Keep +public interface WindowWakeUpPolicyInternal { + + /** + * A delegate that can choose to intercept Input-related wake ups. + * + * <p>This delegate is not meant to control policy decisions on whether or not to wake up. The + * policy makes that decision, and forwards the wake up request to the delegate as necessary. + * Therefore, the role of the delegate is to handle the actual "waking" of the device in + * response to the respective input event. + */ + @Keep + interface InputWakeUpDelegate { + /** + * Wakes up the device in response to a key event. + * + * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}. + * @param keyCode the {@link android.view.KeyEvent} key code of the key event. + * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}. + * @return {@code true} if the delegate handled the wake up. {@code false} if the delegate + * decided not to handle the wake up. The policy will execute the wake up in this case. + */ + boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown); + /** + * Wakes up the device in response to a motion event. + * + * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}. + * @param source the {@link android.view.InputDevice} source that caused the event. + * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}. + * @return {@code true} if the delegate handled the wake up. {@code false} if the delegate + * decided not to handle the wake up. The policy will execute the wake up in this case. + */ + boolean wakeUpFromMotion(long eventTime, int source, boolean isDown); + } + + /** + * Allows injecting a delegate for controlling input-based wake ups. + * + * <p>A delegate can be injected to the policy by system_server components only, and should be + * done via the {@link LocalServices} interface. + * + * <p>There can at most be one active delegate. If there's no delegate set (or if a {@code null} + * delegate is set), the policy will handle waking up the device in response to input events. + * + * @param delegate an implementation of {@link InputWakeUpDelegate} that handles input-based + * wake up requests. {@code null} to let the policy handle these wake ups. + */ + @Keep + void setInputWakeUpDelegate(@Nullable InputWakeUpDelegate delegate); +} diff --git a/services/core/java/com/android/server/policy/window_policy_flags.aconfig b/services/core/java/com/android/server/policy/window_policy_flags.aconfig new file mode 100644 index 000000000000..ed981e0aca74 --- /dev/null +++ b/services/core/java/com/android/server/policy/window_policy_flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.policy" + +flag { + name: "support_input_wakeup_delegate" + namespace: "wear_frameworks" + description: "Whether or not window policy allows injecting input wake-up delegate." + bug: "298055811" +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 03f376358b77..09b19e6196a1 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -190,15 +190,11 @@ public class BatteryStatsImpl extends BatteryStats { // The maximum number of names wakelocks we will keep track of // per uid; once the limit is reached, we batch the remaining wakelocks // in to one common name. - private static final int MAX_WAKELOCKS_PER_UID; + private static final int MAX_WAKELOCKS_PER_UID = isLowRamDevice() ? 40 : 200; - static { - if (ActivityManager.isLowRamDeviceStatic()) { - MAX_WAKELOCKS_PER_UID = 40; - } else { - MAX_WAKELOCKS_PER_UID = 200; - } - } + private static final int CELL_SIGNAL_STRENGTH_LEVEL_COUNT = getCellSignalStrengthLevelCount(); + + private static final int MODEM_TX_POWER_LEVEL_COUNT = getModemTxPowerLevelCount(); // Number of transmit power states the Wifi controller can be in. private static final int NUM_WIFI_TX_LEVELS = 1; @@ -278,11 +274,9 @@ public class BatteryStatsImpl extends BatteryStats { @VisibleForTesting protected KernelSingleUidTimeReader mKernelSingleUidTimeReader; @VisibleForTesting - protected SystemServerCpuThreadReader mSystemServerCpuThreadReader = - SystemServerCpuThreadReader.create(); + protected SystemServerCpuThreadReader mSystemServerCpuThreadReader; - private final KernelMemoryBandwidthStats mKernelMemoryBandwidthStats - = new KernelMemoryBandwidthStats(); + private KernelMemoryBandwidthStats mKernelMemoryBandwidthStats; private final LongSparseArray<SamplingTimer> mKernelMemoryStats = new LongSparseArray<>(); private int[] mCpuPowerBracketMap; private final CpuPowerStatsCollector mCpuPowerStatsCollector; @@ -323,7 +317,7 @@ public class BatteryStatsImpl extends BatteryStats { private long mLastRpmStatsUpdateTimeMs = -RPM_STATS_UPDATE_FREQ_MS; /** Container for Rail Energy Data stats. */ - private final RailStats mTmpRailStats = new RailStats(); + private RailStats mTmpRailStats; /** * Estimate UID modem power usage based on their estimated mobile radio active time. @@ -1031,7 +1025,7 @@ public class BatteryStatsImpl extends BatteryStats { int mPhoneSignalStrengthBin = -1; int mPhoneSignalStrengthBinRaw = -1; final StopwatchTimer[] mPhoneSignalStrengthsTimer = - new StopwatchTimer[CellSignalStrength.getNumSignalStrengthLevels()]; + new StopwatchTimer[CELL_SIGNAL_STRENGTH_LEVEL_COUNT]; StopwatchTimer mPhoneSignalScanningTimer; @@ -1723,7 +1717,8 @@ public class BatteryStatsImpl extends BatteryStats { @VisibleForTesting public BatteryStatsImpl(Clock clock, File historyDirectory, @NonNull Handler handler, @NonNull PowerStatsUidResolver powerStatsUidResolver) { - init(clock); + mClock = clock; + initKernelStatsReaders(); mBatteryStatsConfig = new BatteryStatsConfig.Builder().build(); mHandler = handler; mPowerStatsUidResolver = powerStatsUidResolver; @@ -1748,13 +1743,19 @@ public class BatteryStatsImpl extends BatteryStats { mCpuPowerStatsCollector = null; } - private void init(Clock clock) { - mClock = clock; - mCpuUidUserSysTimeReader = new KernelCpuUidUserSysTimeReader(true, clock); - mCpuUidFreqTimeReader = new KernelCpuUidFreqTimeReader(true, clock); - mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, clock); - mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, clock); + private void initKernelStatsReaders() { + if (!isKernelStatsAvailable()) { + return; + } + + mCpuUidUserSysTimeReader = new KernelCpuUidUserSysTimeReader(true, mClock); + mCpuUidFreqTimeReader = new KernelCpuUidFreqTimeReader(true, mClock); + mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, mClock); + mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, mClock); mKernelWakelockReader = new KernelWakelockReader(); + mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create(); + mKernelMemoryBandwidthStats = new KernelMemoryBandwidthStats(); + mTmpRailStats = new RailStats(); } /** @@ -5878,7 +5879,7 @@ public class BatteryStatsImpl extends BatteryStats { @GuardedBy("this") void stopAllPhoneSignalStrengthTimersLocked(int except, long elapsedRealtimeMs) { - for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) { + for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) { if (i == except) { continue; } @@ -8450,7 +8451,7 @@ public class BatteryStatsImpl extends BatteryStats { public ControllerActivityCounterImpl getOrCreateModemControllerActivityLocked() { if (mModemControllerActivity == null) { mModemControllerActivity = new ControllerActivityCounterImpl(mBsi.mClock, - mBsi.mOnBatteryTimeBase, ModemActivityInfo.getNumTxPowerLevels()); + mBsi.mOnBatteryTimeBase, mBsi.MODEM_TX_POWER_LEVEL_COUNT); } return mModemControllerActivity; } @@ -10896,7 +10897,8 @@ public class BatteryStatsImpl extends BatteryStats { @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies, @NonNull PowerStatsUidResolver powerStatsUidResolver) { - init(clock); + mClock = clock; + initKernelStatsReaders(); mBatteryStatsConfig = config; mMonotonicClock = monotonicClock; @@ -10992,7 +10994,7 @@ public class BatteryStatsImpl extends BatteryStats { mDeviceLightIdlingTimer = new StopwatchTimer(mClock, null, -15, null, mOnBatteryTimeBase); mDeviceIdlingTimer = new StopwatchTimer(mClock, null, -12, null, mOnBatteryTimeBase); mPhoneOnTimer = new StopwatchTimer(mClock, null, -3, null, mOnBatteryTimeBase); - for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) { + for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) { mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(mClock, null, -200 - i, null, mOnBatteryTimeBase); } @@ -11012,7 +11014,7 @@ public class BatteryStatsImpl extends BatteryStats { mBluetoothActivity = new ControllerActivityCounterImpl(mClock, mOnBatteryTimeBase, NUM_BT_TX_LEVELS); mModemActivity = new ControllerActivityCounterImpl(mClock, mOnBatteryTimeBase, - ModemActivityInfo.getNumTxPowerLevels()); + MODEM_TX_POWER_LEVEL_COUNT); mMobileRadioActiveTimer = new StopwatchTimer(mClock, null, -400, null, mOnBatteryTimeBase); mMobileRadioActivePerAppTimer = new StopwatchTimer(mClock, null, -401, null, mOnBatteryTimeBase); @@ -11611,7 +11613,7 @@ public class BatteryStatsImpl extends BatteryStats { mFlashlightOnTimer.reset(false, elapsedRealtimeUs); mCameraOnTimer.reset(false, elapsedRealtimeUs); mBluetoothScanTimer.reset(false, elapsedRealtimeUs); - for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) { + for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) { mPhoneSignalStrengthsTimer[i].reset(false, elapsedRealtimeUs); } mPhoneSignalScanningTimer.reset(false, elapsedRealtimeUs); @@ -11834,7 +11836,7 @@ public class BatteryStatsImpl extends BatteryStats { private String[] mWifiIfaces = EmptyArray.STRING; @GuardedBy("mWifiNetworkLock") - private NetworkStats mLastWifiNetworkStats = new NetworkStats(0, -1); + private NetworkStats mLastWifiNetworkStats; private final Object mModemNetworkLock = new Object(); @@ -11842,7 +11844,7 @@ public class BatteryStatsImpl extends BatteryStats { private String[] mModemIfaces = EmptyArray.STRING; @GuardedBy("mModemNetworkLock") - private NetworkStats mLastModemNetworkStats = new NetworkStats(0, -1); + private NetworkStats mLastModemNetworkStats; @VisibleForTesting protected NetworkStats readMobileNetworkStatsLocked( @@ -11875,7 +11877,9 @@ public class BatteryStatsImpl extends BatteryStats { synchronized (mWifiNetworkLock) { final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager); if (latestStats != null) { - delta = latestStats.subtract(mLastWifiNetworkStats); + delta = mLastWifiNetworkStats != null + ? latestStats.subtract(mLastWifiNetworkStats) + : latestStats.subtract(new NetworkStats(0, -1)); mLastWifiNetworkStats = latestStats; } } @@ -12248,7 +12252,9 @@ public class BatteryStatsImpl extends BatteryStats { synchronized (mModemNetworkLock) { final NetworkStats latestStats = readMobileNetworkStatsLocked(networkStatsManager); if (latestStats != null) { - delta = latestStats.subtract(mLastModemNetworkStats); + delta = latestStats.subtract(mLastModemNetworkStats != null + ? mLastModemNetworkStats + : new NetworkStats(0, -1)); mLastModemNetworkStats = latestStats; } } @@ -12301,7 +12307,7 @@ public class BatteryStatsImpl extends BatteryStats { deltaInfo.getSleepTimeMillis()); mModemActivity.getOrCreateRxTimeCounter() .increment(deltaInfo.getReceiveTimeMillis(), elapsedRealtimeMs); - for (int lvl = 0; lvl < ModemActivityInfo.getNumTxPowerLevels(); lvl++) { + for (int lvl = 0; lvl < MODEM_TX_POWER_LEVEL_COUNT; lvl++) { mModemActivity.getOrCreateTxTimeCounters()[lvl] .increment(deltaInfo.getTransmitDurationMillisAtPowerLevel(lvl), elapsedRealtimeMs); @@ -12318,8 +12324,8 @@ public class BatteryStatsImpl extends BatteryStats { mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_IDLE) + deltaInfo.getReceiveTimeMillis() * mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX); - for (int i = 0; i < Math.min(ModemActivityInfo.getNumTxPowerLevels(), - CellSignalStrength.getNumSignalStrengthLevels()); i++) { + for (int i = 0; i < Math.min(MODEM_TX_POWER_LEVEL_COUNT, + CELL_SIGNAL_STRENGTH_LEVEL_COUNT); i++) { energyUsed += deltaInfo.getTransmitDurationMillisAtPowerLevel(i) * mPowerProfile.getAveragePower( PowerProfile.POWER_MODEM_CONTROLLER_TX, i); @@ -12441,7 +12447,7 @@ public class BatteryStatsImpl extends BatteryStats { } if (totalTxPackets > 0 && entry.getTxPackets() > 0) { - for (int lvl = 0; lvl < ModemActivityInfo.getNumTxPowerLevels(); + for (int lvl = 0; lvl < MODEM_TX_POWER_LEVEL_COUNT; lvl++) { long txMs = entry.getTxPackets() * deltaInfo.getTransmitDurationMillisAtPowerLevel(lvl); @@ -12550,7 +12556,7 @@ public class BatteryStatsImpl extends BatteryStats { && deltaInfo.getSpecificInfoFrequencyRange(0) == ServiceState.FREQUENCY_RANGE_UNKNOWN) { // Specific info data unavailable. Proportionally smear Rx and Tx times across each RAT. - final int levelCount = CellSignalStrength.getNumSignalStrengthLevels(); + final int levelCount = CELL_SIGNAL_STRENGTH_LEVEL_COUNT; long[] perSignalStrengthActiveTimeMs = new long[levelCount]; long totalActiveTimeMs = 0; @@ -12726,13 +12732,13 @@ public class BatteryStatsImpl extends BatteryStats { return; } int levelMaxTimeSpent = 0; - for (int i = 1; i < ModemActivityInfo.getNumTxPowerLevels(); i++) { + for (int i = 1; i < MODEM_TX_POWER_LEVEL_COUNT; i++) { if (activityInfo.getTransmitDurationMillisAtPowerLevel(i) > activityInfo.getTransmitDurationMillisAtPowerLevel(levelMaxTimeSpent)) { levelMaxTimeSpent = i; } } - if (levelMaxTimeSpent == ModemActivityInfo.getNumTxPowerLevels() - 1) { + if (levelMaxTimeSpent == MODEM_TX_POWER_LEVEL_COUNT - 1) { mHistory.recordState2StartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE2_CELLULAR_HIGH_TX_POWER_FLAG); } @@ -14821,12 +14827,12 @@ public class BatteryStatsImpl extends BatteryStats { timeInRatMs[i] = getPhoneDataConnectionTime(i, rawRealTimeUs, which) / 1000; } long[] timeInRxSignalStrengthLevelMs = - new long[CellSignalStrength.getNumSignalStrengthLevels()]; + new long[CELL_SIGNAL_STRENGTH_LEVEL_COUNT]; for (int i = 0; i < timeInRxSignalStrengthLevelMs.length; i++) { timeInRxSignalStrengthLevelMs[i] = getPhoneSignalStrengthTime(i, rawRealTimeUs, which) / 1000; } - long[] txTimeMs = new long[Math.min(ModemActivityInfo.getNumTxPowerLevels(), + long[] txTimeMs = new long[Math.min(MODEM_TX_POWER_LEVEL_COUNT, counter.getTxTimeCounters().length)]; long totalTxTimeMs = 0; for (int i = 0; i < txTimeMs.length; i++) { @@ -15458,7 +15464,7 @@ public class BatteryStatsImpl extends BatteryStats { public Constants(Handler handler) { super(handler); - if (ActivityManager.isLowRamDeviceStatic()) { + if (isLowRamDevice()) { MAX_HISTORY_FILES = DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE; MAX_HISTORY_BUFFER = DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB * 1024; } else { @@ -15528,12 +15534,10 @@ public class BatteryStatsImpl extends BatteryStats { KEY_PROC_STATE_CHANGE_COLLECTION_DELAY_MS, DEFAULT_PROC_STATE_CHANGE_COLLECTION_DELAY_MS); MAX_HISTORY_FILES = mParser.getInt(KEY_MAX_HISTORY_FILES, - ActivityManager.isLowRamDeviceStatic() ? - DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE - : DEFAULT_MAX_HISTORY_FILES); + isLowRamDevice() ? DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE + : DEFAULT_MAX_HISTORY_FILES); MAX_HISTORY_BUFFER = mParser.getInt(KEY_MAX_HISTORY_BUFFER_KB, - ActivityManager.isLowRamDeviceStatic() ? - DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB + isLowRamDevice() ? DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB : DEFAULT_MAX_HISTORY_BUFFER_KB) * 1024; final String perUidModemModel = mParser.getString(KEY_PER_UID_MODEM_POWER_MODEL, @@ -16014,7 +16018,7 @@ public class BatteryStatsImpl extends BatteryStats { mDeviceLightIdlingTimer.readSummaryFromParcelLocked(in); mDeviceIdlingTimer.readSummaryFromParcelLocked(in); mPhoneOnTimer.readSummaryFromParcelLocked(in); - for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) { + for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) { mPhoneSignalStrengthsTimer[i].readSummaryFromParcelLocked(in); } mPhoneSignalScanningTimer.readSummaryFromParcelLocked(in); @@ -16520,7 +16524,7 @@ public class BatteryStatsImpl extends BatteryStats { mDeviceLightIdlingTimer.writeSummaryFromParcelLocked(out, nowRealtime); mDeviceIdlingTimer.writeSummaryFromParcelLocked(out, nowRealtime); mPhoneOnTimer.writeSummaryFromParcelLocked(out, nowRealtime); - for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) { + for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) { mPhoneSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, nowRealtime); } mPhoneSignalScanningTimer.writeSummaryFromParcelLocked(out, nowRealtime); @@ -17015,7 +17019,7 @@ public class BatteryStatsImpl extends BatteryStats { mDeviceIdlingTimer.logState(pr, " "); pr.println("*** Phone timer:"); mPhoneOnTimer.logState(pr, " "); - for (int i = 0; i < CellSignalStrength.getNumSignalStrengthLevels(); i++) { + for (int i = 0; i < CELL_SIGNAL_STRENGTH_LEVEL_COUNT; i++) { pr.println("*** Phone signal strength #" + i + ":"); mPhoneSignalStrengthsTimer[i].logState(pr, " "); } diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 5c603c2238bd..e3eb5b52bc2e 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -1440,6 +1440,13 @@ 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); } @@ -1884,7 +1891,8 @@ public class TrustManagerService extends SystemService { }; } - private final PackageMonitor mPackageMonitor = new PackageMonitor() { + @VisibleForTesting + final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override public void onSomePackagesChanged() { refreshAgentList(UserHandle.USER_ALL); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index b0b66cfa247f..5c867017f4e0 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -130,6 +130,28 @@ class WallpaperData { */ final Rect cropHint = new Rect(0, 0, 0, 0); + // Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked + enum BindSource { + UNKNOWN, + CONNECT_LOCKED, + CONNECTION_TRY_TO_REBIND, + INITIALIZE_FALLBACK, + PACKAGE_UPDATE_FINISHED, + RESTORE_SETTINGS_LIVE_FAILURE, + RESTORE_SETTINGS_LIVE_SUCCESS, + RESTORE_SETTINGS_STATIC, + SET_LIVE, + SET_LIVE_TO_CLEAR, + SET_STATIC, + SWITCH_WALLPAPER_FAILURE, + SWITCH_WALLPAPER_SWITCH_USER, + SWITCH_WALLPAPER_UNLOCK_USER, + } + + // Context in which this wallpaper was bound. Intended for use in resolving b/301073479 but may + // be useful after the issue is resolved as well. + BindSource mBindSource = BindSource.UNKNOWN; + // map of which -> File private final SparseArray<File> mWallpaperFiles = new SparseArray<>(); private final SparseArray<File> mCropFiles = new SparseArray<>(); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index 5f8bbe5f18ad..de98df55c3ea 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -46,6 +46,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.JournaledFile; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.wallpaper.WallpaperData.BindSource; import libcore.io.IoUtils; @@ -314,6 +315,14 @@ class WallpaperDataParser { wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0); wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0); wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f); + BindSource bindSource; + try { + bindSource = Enum.valueOf(BindSource.class, + getAttributeString(parser, "bindSource", BindSource.UNKNOWN.name())); + } catch (IllegalArgumentException | NullPointerException e) { + bindSource = BindSource.UNKNOWN; + } + wallpaper.mBindSource = bindSource; int dimAmountsCount = getAttributeInt(parser, "dimAmountsCount", 0); if (dimAmountsCount > 0) { SparseArray<Float> allDimAmounts = new SparseArray<>(dimAmountsCount); @@ -364,6 +373,11 @@ class WallpaperDataParser { return parser.getAttributeFloat(null, name, defValue); } + private String getAttributeString(XmlPullParser parser, String name, String defValue) { + String s = parser.getAttributeValue(null, name); + return (s != null) ? s : defValue; + } + void saveSettingsLocked(int userId, WallpaperData wallpaper, WallpaperData lockWallpaper) { JournaledFile journal = makeJournaledFile(userId); FileOutputStream fstream = null; @@ -423,6 +437,7 @@ class WallpaperDataParser { } out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount); + out.attribute(null, "bindSource", wallpaper.mBindSource.name()); int dimAmountsCount = wallpaper.mUidToDimAmount.size(); out.attributeInt(null, "dimAmountsCount", dimAmountsCount); if (dimAmountsCount > 0) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 1485b961789c..3782b429f93a 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -122,6 +122,7 @@ import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; +import com.android.server.wallpaper.WallpaperData.BindSource; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -335,6 +336,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub }; // If this was the system wallpaper, rebind... + wallpaper.mBindSource = BindSource.SET_STATIC; bindWallpaperComponentLocked(mImageWallpaper, true, false, wallpaper, callback); } @@ -354,6 +356,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } }; + wallpaper.mBindSource = BindSource.SET_STATIC; bindWallpaperComponentLocked(mImageWallpaper, true /* force */, false /* fromUser */, wallpaper, callback); } else if (isAppliedToLock) { @@ -811,6 +814,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.w(TAG, "Failed attaching wallpaper on display", e); if (wallpaper != null && !wallpaper.wallpaperUpdating && connection.getConnectedEngineSize() == 0) { + wallpaper.mBindSource = BindSource.CONNECT_LOCKED; bindWallpaperComponentLocked(null /* componentName */, false /* force */, false /* fromUser */, wallpaper, null /* reply */); } @@ -1035,6 +1039,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final ComponentName wpService = mWallpaper.wallpaperComponent; // The broadcast of package update could be delayed after service disconnected. Try // to re-bind the service for 10 seconds. + mWallpaper.mBindSource = BindSource.CONNECTION_TRY_TO_REBIND; if (bindWallpaperComponentLocked( wpService, true, false, mWallpaper, null)) { mWallpaper.connection.scheduleTimeoutLocked(); @@ -1321,6 +1326,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } wallpaper.wallpaperUpdating = false; clearWallpaperComponentLocked(wallpaper); + wallpaper.mBindSource = BindSource.PACKAGE_UPDATE_FINISHED; if (!bindWallpaperComponentLocked(wpService, false, false, wallpaper, null)) { Slog.w(TAG, "Wallpaper " + wpService @@ -1711,6 +1717,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (mHomeWallpaperWaitingForUnlock) { final WallpaperData systemWallpaper = getWallpaperSafeLocked(userId, FLAG_SYSTEM); + systemWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER; switchWallpaper(systemWallpaper, null); // TODO(b/278261563): call notifyCallbacksLocked inside switchWallpaper notifyCallbacksLocked(systemWallpaper); @@ -1718,6 +1725,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (mLockWallpaperWaitingForUnlock) { final WallpaperData lockWallpaper = getWallpaperSafeLocked(userId, FLAG_LOCK); + lockWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER; switchWallpaper(lockWallpaper, null); notifyCallbacksLocked(lockWallpaper); } @@ -1838,6 +1846,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // delete them in order to show the default wallpaper. clearWallpaperBitmaps(wallpaper); + fallback.mBindSource = BindSource.SWITCH_WALLPAPER_FAILURE; bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply); if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = true; if ((wallpaper.mWhich & FLAG_LOCK) != 0) mLockWallpaperWaitingForUnlock = true; @@ -2963,6 +2972,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub */ boolean forceRebind = force || (same && systemIsBoth && which == FLAG_SYSTEM); + newWallpaper.mBindSource = + (name == null) ? BindSource.SET_LIVE_TO_CLEAR : BindSource.SET_LIVE; bindSuccess = bindWallpaperComponentLocked(name, /* force */ forceRebind, /* fromUser */ true, newWallpaper, reply); if (bindSuccess) { @@ -3530,6 +3541,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mFallbackWallpaper = new WallpaperData(systemUserId, FLAG_SYSTEM); mFallbackWallpaper.allowBackup = false; mFallbackWallpaper.wallpaperId = makeWallpaperIdLocked(); + mFallbackWallpaper.mBindSource = BindSource.INITIALIZE_FALLBACK; bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false, mFallbackWallpaper, null); } @@ -3553,11 +3565,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.allowBackup = true; // by definition if it was restored if (wallpaper.nextWallpaperComponent != null && !wallpaper.nextWallpaperComponent.equals(mImageWallpaper)) { + wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_SUCCESS; if (!bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, false, false, wallpaper, null)) { // No such live wallpaper or other failure; fall back to the default // live wallpaper (since the profile being restored indicated that the // user had selected a live rather than static one). + wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_FAILURE; bindWallpaperComponentLocked(null, false, false, wallpaper, null); } success = true; @@ -3575,6 +3589,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub + " id=" + wallpaper.wallpaperId); if (success) { mWallpaperCropper.generateCrop(wallpaper); // based on the new image + metadata + wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_STATIC; bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, true, false, wallpaper, null); } @@ -3608,7 +3623,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub pw.print(" User "); pw.print(wallpaper.userId); pw.print(": id="); pw.print(wallpaper.wallpaperId); pw.print(": mWhich="); pw.print(wallpaper.mWhich); - pw.print(": mSystemWasBoth="); pw.println(wallpaper.mSystemWasBoth); + pw.print(": mSystemWasBoth="); pw.print(wallpaper.mSystemWasBoth); + pw.print(": mBindSource="); pw.println(wallpaper.mBindSource.name()); pw.println(" Display state:"); mWallpaperDisplayHelper.forEachDisplayData(wpSize -> { pw.print(" displayId="); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5fa2610cc17b..3a792d079db2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2495,7 +2495,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SnapshotStartingData"); mStartingData = new SnapshotStartingData(mWmService, snapshot, typeParams); - if (task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) { + if ((!mStyleFillsParent && task.getChildCount() > 1) + || task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) { + // Case 1: + // If it is moving a Task{[0]=main activity, [1]=translucent activity} to front, use + // shared starting window so that the transition doesn't need to wait for the activity + // behind the translucent activity. Also, onFirstWindowDrawn will check all visible + // activities are drawn in the task to remove the snapshot starting window. + // Case 2: // Associate with the task so if this activity is resized by task fragment later, the // starting window can keep the same bounds as the task. associateStartingDataWithTask(); @@ -10616,6 +10623,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { + if (task != null && task.mSharedStartingData != null) { + final WindowState startingWin = task.topStartingWindow(); + if (startingWin != null && startingWin.mSyncState == SYNC_STATE_READY + && mDisplayContent.mUnknownAppVisibilityController.allResolved()) { + // The sync is ready if a drawn starting window covered the task. + return true; + } + } if (!super.isSyncFinished(group)) return false; if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController .isVisibilityUnknown(this)) { diff --git a/services/core/java/com/android/server/wm/SensitiveContentPackages.java b/services/core/java/com/android/server/wm/SensitiveContentPackages.java new file mode 100644 index 000000000000..3862b82512c3 --- /dev/null +++ b/services/core/java/com/android/server/wm/SensitiveContentPackages.java @@ -0,0 +1,83 @@ +/* + * 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.wm; + +import android.annotation.NonNull; +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 + * not threadsafe and any call site should hold {@link WindowManagerGlobalLock} + */ +public class SensitiveContentPackages { + private final ArraySet<PackageInfo> mProtectedPackages = new ArraySet<>(); + + /** Returns {@code true} if package/uid pair should be blocked from screen capture */ + public boolean shouldBlockScreenCaptureForApp(String pkg, int uid) { + for (int i = 0; i < mProtectedPackages.size(); i++) { + PackageInfo info = mProtectedPackages.valueAt(i); + if (info != null && info.mPkg.equals(pkg) && info.mUid == uid) { + return true; + } + } + 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(); + mProtectedPackages.addAll(packageInfos); + } + + void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println("SensitiveContentPackages:"); + pw.println(innerPrefix + "Packages that should block screen capture (" + + mProtectedPackages.size() + "):"); + for (PackageInfo info : mProtectedPackages) { + pw.println(innerPrefix + " package=" + info.mPkg + " uid=" + info.mUid); + } + } + + /** Helper class that represents a package/uid pair */ + public static class PackageInfo { + private String mPkg; + private int mUid; + + public PackageInfo(String pkg, int uid) { + this.mPkg = pkg; + this.mUid = uid; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PackageInfo)) return false; + PackageInfo that = (PackageInfo) o; + return mUid == that.mUid && Objects.equals(mPkg, that.mPkg); + } + + @Override + public int hashCode() { + return Objects.hash(mPkg, mUid); + } + } +} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d556f095ae50..a7a6bf2ed2a1 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1411,12 +1411,13 @@ class Task extends TaskFragment { return isUidPresent; } + WindowState topStartingWindow() { + return getWindow(w -> w.mAttrs.type == TYPE_APPLICATION_STARTING); + } + ActivityRecord topActivityContainsStartingWindow() { - if (getParent() == null) { - return null; - } - return getActivity((r) -> r.getWindow(window -> - window.getBaseType() == TYPE_APPLICATION_STARTING) != null); + final WindowState startingWindow = topStartingWindow(); + return startingWindow != null ? startingWindow.mActivityRecord : null; } /** @@ -3698,6 +3699,16 @@ class Task extends TaskFragment { } wc.assignLayer(t, layer++); + // Boost the adjacent TaskFragment for dimmer if needed. + final TaskFragment taskFragment = wc.asTaskFragment(); + if (taskFragment != null && taskFragment.isEmbedded() + && taskFragment.isVisibleRequested()) { + final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); + if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) { + adjacentTf.assignLayer(t, layer++); + } + } + // Place the decor surface just above the owner TaskFragment. if (mDecorSurfaceContainer != null && !decorSurfacePlaced && wc == mDecorSurfaceContainer.mOwnerTaskFragment) { @@ -4784,6 +4795,7 @@ class Task extends TaskFragment { } if (top.isAttached()) { top.setWindowingMode(WINDOWING_MODE_UNDEFINED); + top.mWaitForEnteringPinnedMode = false; } } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index f51bd1be158c..f56759f9481c 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -39,6 +39,7 @@ import static android.os.Process.INVALID_UID; import static android.os.Process.SYSTEM_UID; import static android.os.UserHandle.USER_NULL; import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; import static android.view.WindowManager.TRANSIT_NONE; @@ -2995,6 +2996,30 @@ class TaskFragment extends WindowContainer<WindowContainer> { }, false /* traverseTopToBottom */); } + boolean shouldBoostDimmer() { + if (asTask() != null || !isDimmingOnParentTask()) { + // early return if not embedded or should not dim on parent Task. + return false; + } + + final TaskFragment adjacentTf = getAdjacentTaskFragment(); + if (adjacentTf == null) { + // early return if no adjacent TF. + return false; + } + + if (getParent().mChildren.indexOf(adjacentTf) < getParent().mChildren.indexOf(this)) { + // early return if this TF already has higher z-ordering. + return false; + } + + // boost if there's an Activity window that has FLAG_DIM_BEHIND flag. + return forAllWindows( + (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null + && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested() + || w.mActivityRecord.isVisible()), true); + } + @Override Dimmer getDimmer() { // If this is in an embedded TaskFragment and we want the dim applies on the TaskFragment. diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 56bef3335b8b..e1bf8f8aa34a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2596,6 +2596,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { change.setBackgroundColor(ColorUtils.setAlphaComponent(backgroundColor, 255)); } + if (activityRecord != null) { + change.setActivityComponent(activityRecord.mActivityComponent); + } + change.setRotation(info.mRotation, endRotation); if (info.mSnapshot != null) { change.setSnapshot(info.mSnapshot, info.mSnapshotLuma); diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 33ef3c5629e3..a9f0554b2bec 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -183,18 +183,21 @@ class WallpaperController { && (mWallpaperTarget == w || w.isDrawFinishedLw())) { if (DEBUG_WALLPAPER) Slog.v(TAG, "Found wallpaper target: " + w); mFindResults.setWallpaperTarget(w); + mFindResults.setIsWallpaperTargetForLetterbox(w.hasWallpaperForLetterboxBackground()); if (w == mWallpaperTarget && w.isAnimating(TRANSITION | PARENTS)) { // The current wallpaper target is animating, so we'll look behind it for // another possible target and figure out what is going on later. if (DEBUG_WALLPAPER) Slog.v(TAG, "Win " + w + ": token animating, looking behind."); } - mFindResults.setIsWallpaperTargetForLetterbox(w.hasWallpaperForLetterboxBackground()); // While the keyguard is going away, both notification shade and a normal activity such // as a launcher can satisfy criteria for a wallpaper target. In this case, we should // chose the normal activity, otherwise wallpaper becomes invisible when a new animation // starts before the keyguard going away animation finishes. - return w.mActivityRecord != null; + if (w.mActivityRecord == null && mDisplayContent.isKeyguardGoingAway()) { + return false; + } + return true; } return false; }; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index e28262dfbe2f..bdea1bc40f3a 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3938,7 +3938,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< @Nullable BLASTSyncEngine.SyncGroup getSyncGroup() { if (mSyncGroup != null) return mSyncGroup; - if (mParent != null) return mParent.getSyncGroup(); + WindowContainer<?> parent = mParent; + while (parent != null) { + if (parent.mSyncGroup != null) { + return parent.mSyncGroup; + } + parent = parent.mParent; + } return null; } @@ -3972,7 +3978,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * @param cancel If true, this is being finished because it is leaving the sync group rather * than due to the sync group completing. */ - void finishSync(Transaction outMergedTransaction, BLASTSyncEngine.SyncGroup group, + void finishSync(Transaction outMergedTransaction, @Nullable BLASTSyncEngine.SyncGroup group, boolean cancel) { if (mSyncState == SYNC_STATE_NONE) return; final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup(); @@ -4000,7 +4006,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (!isVisibleRequested()) { return true; } - if (mSyncState == SYNC_STATE_NONE) { + if (mSyncState == SYNC_STATE_NONE && getSyncGroup() != null) { + Slog.i(TAG, "prepareSync in isSyncFinished: " + this); prepareSync(); } if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW) { @@ -4059,16 +4066,18 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } if (newParent == null) { // This is getting removed. + final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup(); if (oldParent.mSyncState != SYNC_STATE_NONE) { // In order to keep the transaction in sync, merge it into the parent. - finishSync(oldParent.mSyncTransaction, getSyncGroup(), true /* cancel */); - } else if (mSyncGroup != null) { - // This is watched directly by the sync-group, so merge this transaction into - // into the sync-group so it isn't lost - finishSync(mSyncGroup.getOrphanTransaction(), mSyncGroup, true /* cancel */); + finishSync(oldParent.mSyncTransaction, syncGroup, true /* cancel */); + } else if (syncGroup != null) { + // This is watched by the sync-group, so merge this transaction into the + // sync-group for not losing the operations in the transaction. + finishSync(syncGroup.getOrphanTransaction(), syncGroup, true /* cancel */); } else { - throw new IllegalStateException("This container is in sync mode without a sync" - + " group: " + this); + Slog.wtf(TAG, this + " is in sync mode without a sync group"); + // Make sure the removal transaction take effect. + finishSync(getPendingTransaction(), null /* group */, true /* cancel */); } return; } else if (mSyncGroup == null) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 516d37c0136a..22b690e85c35 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -51,6 +51,7 @@ import android.window.ScreenCapture; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.input.InputManagerService; import com.android.server.policy.WindowManagerPolicy; +import com.android.server.wm.SensitiveContentPackages.PackageInfo; import java.lang.annotation.Retention; import java.util.List; @@ -1012,4 +1013,12 @@ public abstract class WindowManagerInternal { */ public abstract void setOrientationRequestPolicy(boolean respected, int[] fromOrientations, int[] toOrientations); + + /** + * Set whether screen capture should be disabled for all windows of a specific app windows based + * on sensitive content protections. + * + * @param packageInfos set of {@link PackageInfo} whose windows should be blocked from capture + */ + public abstract void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 502912a98816..c63cc4373472 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -123,6 +123,7 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS; +import static com.android.server.wm.SensitiveContentPackages.PackageInfo; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; @@ -312,6 +313,7 @@ import android.window.WindowContainerToken; import android.window.WindowContextInfo; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.policy.IKeyguardDismissCallback; @@ -366,6 +368,7 @@ 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; @@ -1053,6 +1056,9 @@ public class WindowManagerService extends IWindowManager.Stub SystemPerformanceHinter mSystemPerformanceHinter; + @GuardedBy("mGlobalLock") + final SensitiveContentPackages mSensitiveContentPackages = new SensitiveContentPackages(); + /** Listener to notify activity manager about app transitions. */ final WindowManagerInternal.AppTransitionListener mActivityManagerAppTransitionNotifier = new WindowManagerInternal.AppTransitionListener() { @@ -1797,7 +1803,12 @@ public class WindowManagerService extends IWindowManager.Stub // Don't do layout here, the window must call // relayout to be displayed, so we'll do it there. - win.getParent().assignChildLayers(); + if (win.mActivityRecord != null && win.mActivityRecord.isEmbedded()) { + // Assign child layers from the parent Task if the Activity is embedded. + win.getTask().assignChildLayers(); + } else { + win.getParent().assignChildLayers(); + } if (focusChanged) { displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus, @@ -1931,12 +1942,13 @@ public class WindowManagerService extends IWindowManager.Stub /** * Set whether screen capture is disabled for all windows of a specific user from - * the device policy cache. + * the device policy cache, or specific windows based on sensitive content protections. */ @Override public void refreshScreenCaptureDisabled() { int callingUid = Binder.getCallingUid(); - if (callingUid != SYSTEM_UID) { + // MY_UID (Process.myUid()) should always be SYSTEM_UID here, but using MY_UID for tests + if (callingUid != MY_UID) { throw new SecurityException("Only system can call refreshScreenCaptureDisabled."); } @@ -7169,6 +7181,7 @@ public class WindowManagerService extends IWindowManager.Stub } mSystemPerformanceHinter.dump(pw, ""); mTrustedPresentationListenerController.dump(pw); + mSensitiveContentPackages.dump(pw); } } @@ -8550,6 +8563,14 @@ public class WindowManagerService extends IWindowManager.Stub InputTarget inputTarget = WindowManagerService.this.getInputTargetFromToken(inputToken); return inputTarget == null ? null : inputTarget.getWindowToken(); } + + @Override + public void setShouldBlockScreenCaptureForApp(Set<PackageInfo> packageInfos) { + synchronized (mGlobalLock) { + mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(packageInfos); + WindowManagerService.this.refreshScreenCaptureDisabled(); + } + } } private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 0b43be700b0d..56f2bc3d3e3b 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1896,6 +1896,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) { return true; } + + if (com.android.server.notification.Flags.sensitiveNotificationAppProtection()) { + if (mWmService.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(getOwningPackage(), getOwningUid())) { + return true; + } + } + return !DevicePolicyCache.getInstance().isScreenCaptureAllowed(mShowUserId); } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index dfa9dcecfbb5..775570cb08ba 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -37,6 +37,7 @@ 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", @@ -94,6 +95,16 @@ 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 cc08488742b2..15eb7c64a40c 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -33,3 +33,7 @@ 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 new file mode 100644 index 000000000000..3892d284dafb --- /dev/null +++ b/services/core/jni/com_android_server_BootReceiver.cpp @@ -0,0 +1,57 @@ +/* + * 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_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp index 97b18fac91f4..1e48aced0041 100644 --- a/services/core/jni/com_android_server_utils_AnrTimer.cpp +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -486,9 +486,11 @@ class AnrTimerService::Ticker { void remove(AnrTimerService const* service) { AutoMutex _l(lock_); timer_id_t front = headTimerId(); - for (auto i = running_.begin(); i != running_.end(); i++) { + for (auto i = running_.begin(); i != running_.end(); ) { if (i->service == service) { - running_.erase(i); + i = running_.erase(i); + } else { + i++; } } if (front != headTimerId()) restartLocked(); diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index f3158d11b9a4..a4b1f841d3bc 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -52,10 +52,10 @@ int register_android_server_Watchdog(JNIEnv* env); int register_android_server_HardwarePropertiesManagerService(JNIEnv* env); int register_android_server_SyntheticPasswordManager(JNIEnv* env); int register_android_hardware_display_DisplayViewport(JNIEnv* env); -int register_android_server_utils_AnrTimer(JNIEnv *env); int register_android_server_am_OomConnection(JNIEnv* env); int register_android_server_am_CachedAppOptimizer(JNIEnv* env); int register_android_server_am_LowMemDetector(JNIEnv* env); +int register_android_server_utils_AnrTimer(JNIEnv *env); int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env); int register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(JNIEnv* env); int register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(JNIEnv* env); @@ -66,6 +66,7 @@ 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); @@ -114,10 +115,10 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_storage_AppFuse(env); register_android_server_SyntheticPasswordManager(env); register_android_hardware_display_DisplayViewport(env); - register_android_server_utils_AnrTimer(env); register_android_server_am_OomConnection(env); register_android_server_am_CachedAppOptimizer(env); register_android_server_am_LowMemDetector(env); + register_android_server_utils_AnrTimer(env); register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env); register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(env); register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(env); @@ -128,6 +129,7 @@ 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/core/xsd/display-layout-config/display-layout-config.xsd b/services/core/xsd/display-layout-config/display-layout-config.xsd index 4e954653bcbb..73c13cec6bc2 100644 --- a/services/core/xsd/display-layout-config/display-layout-config.xsd +++ b/services/core/xsd/display-layout-config/display-layout-config.xsd @@ -49,7 +49,7 @@ <xs:complexType name="display"> <xs:sequence> - <xs:element name="address" type="xs:nonNegativeInteger"/> + <xs:group ref="displayReference" minOccurs="1" maxOccurs="1"/> <xs:element name="position" type="xs:string" minOccurs="0" maxOccurs="1" /> <xs:element name="brightnessThrottlingMapId" type="xs:string" minOccurs="0" maxOccurs="1" /> <xs:element name="powerThrottlingMapId" type="xs:string" minOccurs="0" maxOccurs="1" /> @@ -67,4 +67,11 @@ </xs:simpleType> </xs:attribute> </xs:complexType> + + <xs:group name="displayReference"> + <xs:choice> + <xs:element name="address" type="xs:nonNegativeInteger" minOccurs="0"/> + <xs:element name="port" type="xs:nonNegativeInteger" minOccurs="0"/> + </xs:choice> + </xs:group> </xs:schema> diff --git a/services/core/xsd/display-layout-config/schema/current.txt b/services/core/xsd/display-layout-config/schema/current.txt index 195cae5aee14..0d2f6a89ac25 100644 --- a/services/core/xsd/display-layout-config/schema/current.txt +++ b/services/core/xsd/display-layout-config/schema/current.txt @@ -3,22 +3,24 @@ package com.android.server.display.config.layout { public class Display { ctor public Display(); - method public java.math.BigInteger getAddress(); + method public java.math.BigInteger getAddress_optional(); method public String getBrightnessThrottlingMapId(); method public String getDisplayGroup(); method public java.math.BigInteger getLeadDisplayAddress(); + method public java.math.BigInteger getPort_optional(); method public String getPosition(); method public String getPowerThrottlingMapId(); method public String getRefreshRateThermalThrottlingMapId(); method public String getRefreshRateZoneId(); method public boolean isDefaultDisplay(); method public boolean isEnabled(); - method public void setAddress(java.math.BigInteger); + method public void setAddress_optional(java.math.BigInteger); method public void setBrightnessThrottlingMapId(String); method public void setDefaultDisplay(boolean); method public void setDisplayGroup(String); method public void setEnabled(boolean); method public void setLeadDisplayAddress(java.math.BigInteger); + method public void setPort_optional(java.math.BigInteger); method public void setPosition(String); method public void setPowerThrottlingMapId(String); method public void setRefreshRateThermalThrottlingMapId(String); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index dfb5a5758448..fb0729f806b1 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -517,6 +517,8 @@ public final class CredentialManagerService .map(CredentialOption::getType) .collect(Collectors.toList())); + finalizeAndEmitInitialPhaseMetric(session); + if (providerSessions.isEmpty()) { try { callback.onError( @@ -776,6 +778,13 @@ public final class CredentialManagerService providerSessions.forEach(ProviderSession::invokeSession); } + private void finalizeAndEmitInitialPhaseMetric(GetCandidateRequestSession session) { + var initMetric = session.mRequestSessionMetric.getInitialPhaseMetric(); + initMetric.setAutofillSessionId(session.getAutofillSessionId()); + initMetric.setAutofillRequestId(session.getAutofillRequestId()); + finalizeAndEmitInitialPhaseMetric((RequestSession) session); + } + private void finalizeAndEmitInitialPhaseMetric(RequestSession session) { try { var initMetric = session.mRequestSessionMetric.getInitialPhaseMetric(); diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index d1651713fe03..0f914c32346d 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -49,7 +49,12 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> { private static final String TAG = "GetCandidateRequestSession"; + private static final String SESSION_ID_KEY = "autofill_session_id"; + private static final String REQUEST_ID_KEY = "autofill_request_id"; + private final IAutoFillManagerClient mAutoFillCallback; + private final int mAutofillSessionId; + private final int mAutofillRequestId; public GetCandidateRequestSession( Context context, SessionLifetime sessionCallback, @@ -62,6 +67,8 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ RequestInfo.TYPE_GET, callingAppInfo, enabledProviders, cancellationSignal, 0L); mAutoFillCallback = autoFillCallback; + mAutofillSessionId = request.getData().getInt(SESSION_ID_KEY, -1); + mAutofillRequestId = request.getData().getInt(REQUEST_ID_KEY, -1); } /** @@ -177,4 +184,19 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ Slog.d(TAG, "onFinalResponseReceived"); respondToClientWithResponseAndFinish(new GetCandidateCredentialsResponse(response)); } + + /** + * Returns autofill session id. Returns -1 if unavailable. + */ + public int getAutofillSessionId() { + return mAutofillSessionId; + } + + /** + * Returns autofill request id. Returns -1 if unavailable. + */ + public int getAutofillRequestId() { + return mAutofillRequestId; + + } } diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index b36de0b03fa8..23aa3742175a 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -426,7 +426,11 @@ public class MetricUtilities { /* per_classtype_counts */ initialPhaseMetric.getUniqueRequestCounts(), /* origin_specified */ - initialPhaseMetric.isOriginSpecified() + initialPhaseMetric.isOriginSpecified(), + /* autofill_session_id */ + initialPhaseMetric.getAutofillSessionId(), + /* autofill_request_id */ + initialPhaseMetric.getAutofillRequestId() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during initial metric emit: " + e); diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java index 8e965e3e5ba5..8a4e86c440b3 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java @@ -49,6 +49,12 @@ public class InitialPhaseMetric { // Stores the deduped request information, particularly {"req":5} private Map<String, Integer> mRequestCounts = new LinkedHashMap<>(); + // The session id of autofill if the request is from autofill, defaults to -1 + private int mAutofillSessionId = -1; + + // The request id of autofill if the request is from autofill, defaults to -1 + private int mAutofillRequestId = -1; + public InitialPhaseMetric(int sessionIdTrackOne) { mSessionIdCaller = sessionIdTrackOne; @@ -126,6 +132,24 @@ public class InitialPhaseMetric { return mOriginSpecified; } + /* ------ Autofill Integration ------ */ + + public void setAutofillSessionId(int autofillSessionId) { + mAutofillSessionId = autofillSessionId; + } + + public int getAutofillSessionId() { + return mAutofillSessionId; + } + + public void setAutofillRequestId(int autofillRequestId) { + mAutofillRequestId = autofillRequestId; + } + + public int getAutofillRequestId() { + return mAutofillRequestId; + } + /* ------ Unique Request Counts Map Information ------ */ public void setRequestCounts(Map<String, Integer> requestCounts) { diff --git a/services/java/com/android/server/SystemConfigService.java b/services/java/com/android/server/SystemConfigService.java index 6e82907d4361..fd21a326b640 100644 --- a/services/java/com/android/server/SystemConfigService.java +++ b/services/java/com/android/server/SystemConfigService.java @@ -21,6 +21,8 @@ import static java.util.stream.Collectors.toMap; import android.Manifest; import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; import android.os.ISystemConfig; import android.util.ArrayMap; import android.util.ArraySet; @@ -108,6 +110,15 @@ public class SystemConfigService extends SystemService { "Caller must hold " + Manifest.permission.QUERY_ALL_PACKAGES); return new ArrayList<>(SystemConfig.getInstance().getDefaultVrComponents()); } + + @Override + public List<String> getPreventUserDisablePackages() { + PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + return SystemConfig.getInstance().getPreventUserDisablePackages().stream() + .filter(preventUserDisablePackage -> + pmi.canQueryPackage(Binder.getCallingUid(), preventUserDisablePackage)) + .collect(toList()); + } }; public SystemConfigService(Context context) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 1185a4e4f93b..86ad49458c48 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2965,6 +2965,12 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + if (com.android.server.notification.Flags.sensitiveNotificationAppProtection()) { + t.traceBegin("StartSensitiveContentProtectionManager"); + mSystemServiceManager.startService(SensitiveContentProtectionManagerService.class); + t.traceEnd(); + } + // These are needed to propagate to the runnable below. final NetworkManagementService networkManagementF = networkManagement; final NetworkPolicyManagerService networkPolicyF = networkPolicy; diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java index e5be4d9aa755..9e11fa2f0bdf 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java @@ -50,7 +50,7 @@ import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; -// atest PackageManagerServiceTest:com.android.server.pm.UserDataPreparerTest +// atest PackageManagerServiceServerTests:com.android.server.pm.UserDataPreparerTest @RunWith(AndroidJUnit4.class) @Presubmit @SmallTest @@ -99,7 +99,7 @@ public class UserDataPreparerTest { systemDeDir.mkdirs(); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_DE); verify(mStorageManagerMock).prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), - eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_DE)); + eq(StorageManager.FLAG_STORAGE_DE)); verify(mInstaller).createUserData(isNull(String.class), eq(TEST_USER_ID), eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_DE)); int serialNumber = UserDataPreparer.getSerialNumber(userDeDir); @@ -116,7 +116,7 @@ public class UserDataPreparerTest { systemCeDir.mkdirs(); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE); verify(mStorageManagerMock).prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), - eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_CE)); + eq(StorageManager.FLAG_STORAGE_CE)); verify(mInstaller).createUserData(isNull(String.class), eq(TEST_USER_ID), eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_CE)); int serialNumber = UserDataPreparer.getSerialNumber(userCeDir); @@ -129,7 +129,7 @@ public class UserDataPreparerTest { public void testPrepareUserData_forNewUser_destroysOnFailure() throws Exception { TEST_USER.lastLoggedInTime = 0; doThrow(new IllegalStateException("expected exception for test")).when(mStorageManagerMock) - .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), eq(TEST_USER_SERIAL), + .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE); verify(mStorageManagerMock).destroyUserStorage(isNull(String.class), eq(TEST_USER_ID), @@ -140,7 +140,7 @@ public class UserDataPreparerTest { public void testPrepareUserData_forExistingUser_doesNotDestroyOnFailure() throws Exception { TEST_USER.lastLoggedInTime = System.currentTimeMillis(); doThrow(new IllegalStateException("expected exception for test")).when(mStorageManagerMock) - .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), eq(TEST_USER_SERIAL), + .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE); verify(mStorageManagerMock, never()).destroyUserStorage(isNull(String.class), diff --git a/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java b/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java index 8cc3408ad79b..567792ecb6e0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DeviceStateToLayoutMapTest.java @@ -25,6 +25,7 @@ import android.view.DisplayAddress; import androidx.test.filters.SmallTest; +import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.layout.DisplayIdProducer; import com.android.server.display.layout.Layout; @@ -45,6 +46,7 @@ public class DeviceStateToLayoutMapTest { private DeviceStateToLayoutMap mDeviceStateToLayoutMap; @Mock DisplayIdProducer mDisplayIdProducerMock; + @Mock DisplayManagerFlags mMockFlags; @Before public void setUp() throws IOException { @@ -52,7 +54,7 @@ public class DeviceStateToLayoutMapTest { Mockito.when(mDisplayIdProducerMock.getId(false)).thenReturn(1); - setupDeviceStateToLayoutMap(); + setupDeviceStateToLayoutMap(getContent()); } ////////////////// @@ -268,6 +270,41 @@ public class DeviceStateToLayoutMapTest { IllegalArgumentException.class, () -> layout.postProcessLocked()); } + @Test + public void testPortInLayout_disabledFlag() { + Mockito.when(mMockFlags.isPortInDisplayLayoutEnabled()).thenReturn(false); + assertThrows("Expected IllegalArgumentException when using <port>", + IllegalArgumentException.class, + () -> setupDeviceStateToLayoutMap(getPortContent())); + } + + @Test + public void testPortInLayout_readLayout() throws Exception { + Mockito.when(mMockFlags.isPortInDisplayLayoutEnabled()).thenReturn(true); + setupDeviceStateToLayoutMap(getPortContent()); + + Layout configLayout = mDeviceStateToLayoutMap.get(0); + + Layout testLayout = new Layout(); + testLayout.createDisplayLocked(DisplayAddress.fromPortAndModel(123, null), + /* isDefault= */ true, /* isEnabled= */ true, /* displayGroupName= */ null, + mDisplayIdProducerMock, Layout.Display.POSITION_UNKNOWN, + /* leadDisplayAddress= */ null, /* brightnessThrottlingMapId= */ null, + /* refreshRateZoneId= */ null, + /* refreshRateThermalThrottlingMapId= */ null, + /* powerThrottlingMapId= */ null); + testLayout.createDisplayLocked(DisplayAddress.fromPhysicalDisplayId(78910L), + /* isDefault= */ false, /* isEnabled= */ false, /* displayGroupName= */ null, + mDisplayIdProducerMock, Layout.Display.POSITION_UNKNOWN, + /* leadDisplayAddress= */ null, /* brightnessThrottlingMapId= */ null, + /* refreshRateZoneId= */ null, + /* refreshRateThermalThrottlingMapId= */ null, + /* powerThrottlingMapId= */ null); + testLayout.postProcessLocked(); + + assertEquals(testLayout, configLayout); + } + //////////////////// // Helper Methods // //////////////////// @@ -287,13 +324,28 @@ public class DeviceStateToLayoutMapTest { /* powerThrottlingMapId= */ null); } - private void setupDeviceStateToLayoutMap() throws IOException { + private void setupDeviceStateToLayoutMap(String content) throws IOException { Path tempFile = Files.createTempFile("device_state_layout_map", ".tmp"); - Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8)); - mDeviceStateToLayoutMap = new DeviceStateToLayoutMap(mDisplayIdProducerMock, + Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8)); + mDeviceStateToLayoutMap = new DeviceStateToLayoutMap(mDisplayIdProducerMock, mMockFlags, tempFile.toFile()); } + private String getPortContent() { + return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<layouts>\n" + + "<layout>\n" + + "<state>0</state> \n" + + "<display enabled=\"true\" defaultDisplay=\"true\">\n" + + "<port>123</port>\n" + + "</display>\n" + + "<display enabled=\"false\">\n" + + "<address>78910</address>\n" + + "</display>\n" + + "</layout>\n" + + "</layouts>\n"; + } + private String getContent() { return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + "<layouts>\n" diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java index eb6e8b4469f0..ad4d91ff8ba0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java @@ -96,6 +96,8 @@ public class DisplayBrightnessStateTest { .append(displayBrightnessState.isSlowChange()) .append("\n maxBrightness:") .append(displayBrightnessState.getMaxBrightness()) + .append("\n minBrightness:") + .append(displayBrightnessState.getMinBrightness()) .append("\n customAnimationRate:") .append(displayBrightnessState.getCustomAnimationRate()) .append("\n shouldUpdateScreenBrightnessSetting:") diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index e370f5501865..c67e7c5ae61e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -41,6 +41,7 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.hardware.display.DisplayManagerInternal; +import android.os.PowerManager; import android.os.Temperature; import android.provider.Settings; import android.util.SparseArray; @@ -109,6 +110,43 @@ public final class DisplayDeviceConfigTest { } @Test + public void testDefaultValues() { + when(mResources.getString(com.android.internal.R.string.config_displayLightSensorType)) + .thenReturn("test_light_sensor"); + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true); + + mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, /* useConfigXml= */ false, + mFlags); + + assertEquals(DisplayDeviceConfig.BRIGHTNESS_DEFAULT, + mDisplayDeviceConfig.getBrightnessDefault(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampFastDecrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampFastIncrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowDecreaseIdle(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowIncreaseIdle(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis()); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis()); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampDecreaseMaxIdleMillis()); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampIncreaseMaxIdleMillis()); + assertNull(mDisplayDeviceConfig.getNits()); + assertNull(mDisplayDeviceConfig.getBacklight()); + assertEquals(0.3f, mDisplayDeviceConfig.getBacklightFromBrightness(0.3f), ZERO_DELTA); + assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type); + assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name); + assertNull(mDisplayDeviceConfig.getProximitySensor().type); + assertNull(mDisplayDeviceConfig.getProximitySensor().name); + assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); + } + + @Test public void testConfigValuesFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(); @@ -681,6 +719,7 @@ public final class DisplayDeviceConfigTest { assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type); assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name); + assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); assertEquals(brightnessIntToFloat(35), mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA); @@ -807,6 +846,24 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), SMALL_DELTA); } + @Test + public void testIsAutoBrightnessAvailable_EnabledInConfigResource() throws IOException { + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true); + + setupDisplayDeviceConfigFromDisplayConfigFile(); + + assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); + } + + @Test + public void testIsAutoBrightnessAvailable_DisabledInConfigResource() throws IOException { + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(false); + + setupDisplayDeviceConfigFromDisplayConfigFile(); + + assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable()); + } + private String getValidLuxThrottling() { return "<luxThrottling>\n" + " <brightnessLimitMap>\n" @@ -1176,7 +1233,7 @@ public final class DisplayDeviceConfigTest { + "<nits>" + NITS[2] + "</nits>\n" + "</point>\n" + "</screenBrightnessMap>\n" - + "<autoBrightness>\n" + + "<autoBrightness enabled=\"true\">\n" + "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n" + "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n" + (includeIdleMode ? getRampSpeedsIdle() : "") @@ -1593,6 +1650,7 @@ public final class DisplayDeviceConfigTest { when(mResources.getString(com.android.internal.R.string.config_displayLightSensorType)) .thenReturn("test_light_sensor"); + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true); when(mResources.getInteger( R.integer.config_autoBrightnessBrighteningLightDebounce)) diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt index dafbbb3e0140..33d30200faaa 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt @@ -16,16 +16,24 @@ package com.android.server.display +import android.content.Context +import android.os.Looper import android.view.Display import androidx.test.filters.SmallTest import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.util.concurrent.Executor @SmallTest @@ -39,11 +47,16 @@ class DisplayPowerStateTest { private val mockBlanker = mock<DisplayBlanker>() private val mockColorFade = mock<ColorFade>() private val mockExecutor = mock<Executor>() + private val mockContext = mock<Context>() @Before fun setUp() { + if (Looper.myLooper() == null) { + Looper.prepare() + } displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON, mockExecutor) + whenever(mockColorFade.prepare(eq(mockContext), anyInt())).thenReturn(true) } @Test @@ -56,4 +69,31 @@ class DisplayPowerStateTest { verify(mockColorFade).destroy() } + + @Test + fun `GIVEN not prepared WHEN draw runnable is called THEN colorFade not drawn`() { + displayPowerState.mColorFadeDrawRunnable.run() + + verify(mockColorFade, never()).draw(anyFloat()) + } + @Test + fun `GIVEN prepared WHEN draw runnable is called THEN colorFade is drawn`() { + displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE) + clearInvocations(mockColorFade) + + displayPowerState.mColorFadeDrawRunnable.run() + + verify(mockColorFade).draw(anyFloat()) + } + + @Test + fun `GIVEN prepared AND stopped WHEN draw runnable is called THEN colorFade is not drawn`() { + displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE) + clearInvocations(mockColorFade) + displayPowerState.stop() + + displayPowerState.mColorFadeDrawRunnable.run() + + verify(mockColorFade, never()).draw(anyFloat()) + } }
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 00f98924eff3..c92ce254cae4 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -176,6 +176,10 @@ public class LocalDisplayAdapterTest { when(mockArray.length()).thenReturn(0); when(mMockedResources.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray)) .thenReturn(mockArray); + when(mMockedResources.obtainTypedArray(R.array.config_displayCutoutSideOverrideArray)) + .thenReturn(mockArray); + when(mMockedResources.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride)) + .thenReturn(new String[]{}); when(mMockedResources.obtainTypedArray(R.array.config_waterfallCutoutArray)) .thenReturn(mockArray); when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerRadiusArray)) diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 28ec89629df0..bed6f928a55c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -49,8 +49,9 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -83,7 +84,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.Spy; import java.io.File; import java.io.InputStream; @@ -111,14 +111,14 @@ public class LogicalDisplayMapperTest { private final DisplayIdProducer mIdProducer = (isDefault) -> isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++; + private DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy; + @Mock LogicalDisplayMapper.Listener mListenerMock; @Mock Context mContextMock; @Mock FoldSettingProvider mFoldSettingProviderMock; @Mock Resources mResourcesMock; @Mock IPowerManager mIPowerManagerMock; @Mock IThermalService mIThermalServiceMock; - @Spy DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy = - new DeviceStateToLayoutMap(mIdProducer, NON_EXISTING_FILE); @Mock DisplayManagerFlags mFlagsMock; @Mock DisplayAdapter mDisplayAdapterMock; @@ -131,6 +131,8 @@ public class LogicalDisplayMapperTest { System.setProperty("dexmaker.share_classloader", "true"); MockitoAnnotations.initMocks(this); + mDeviceStateToLayoutMapSpy = + spy(new DeviceStateToLayoutMap(mIdProducer, mFlagsMock, NON_EXISTING_FILE)); mDisplayDeviceRepo = new DisplayDeviceRepository( new DisplayManagerService.SyncRoot(), new PersistentDataStore(new PersistentDataStore.Injector() { diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java index e58b3e891b70..990c3830b76c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessReasonTest.java @@ -58,9 +58,14 @@ public final class BrightnessReasonTest { @Test public void setModifierDoesntSetIfModifierIsBeyondExtremes() { - int extremeModifier = 0x16; + int extremeModifier = 0x40; // equal to BrightnessReason.MODIFIER_MASK * 2 + + // reset modifier + mBrightnessReason.setModifier(0); + + // test extreme mBrightnessReason.setModifier(extremeModifier); - assertEquals(mBrightnessReason.getModifier(), BrightnessReason.MODIFIER_LOW_POWER); + assertEquals(0, mBrightnessReason.getModifier()); } @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index 6ba7368f8f26..5294943fa387 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -29,8 +29,10 @@ import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.PowerManager; import android.provider.DeviceConfig; +import android.testing.TestableContext; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; @@ -39,6 +41,7 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.testutils.TestHandler; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -52,14 +55,15 @@ public class BrightnessClamperControllerTest { private final TestHandler mTestHandler = new TestHandler(null); + @Rule + public final TestableContext mMockContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getContext()); @Mock private BrightnessClamperController.ClamperChangeListener mMockExternalListener; @Mock private BrightnessClamperController.DisplayDeviceData mMockDisplayDeviceData; @Mock - private Context mMockContext; - @Mock private DeviceConfigParameterProvider mMockDeviceConfigParameterProvider; @Mock private BrightnessClamper<BrightnessClamperController.DisplayDeviceData> mMockClamper; @@ -231,6 +235,13 @@ public class BrightnessClamperControllerTest { assertEquals(initialSlowChange, state.isSlowChange()); } + @Test + public void testStop() { + mClamperController.stop(); + verify(mMockModifier).stop(); + verify(mMockClamper).stop(); + } + private BrightnessClamperController createBrightnessClamperController() { return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener, mMockDisplayDeviceData, mMockContext, mFlags); @@ -240,14 +251,14 @@ public class BrightnessClamperControllerTest { private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>> mClampers; - private final List<BrightnessModifier> mModifiers; + private final List<BrightnessStateModifier> mModifiers; private BrightnessClamperController.ClamperChangeListener mCapturedChangeListener; private TestInjector( List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>> clampers, - List<BrightnessModifier> modifiers) { + List<BrightnessStateModifier> modifiers) { mClampers = clampers; mModifiers = modifiers; } @@ -268,7 +279,8 @@ public class BrightnessClamperControllerTest { } @Override - List<BrightnessModifier> getModifiers(Context context) { + List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, + Handler handler, BrightnessClamperController.ClamperChangeListener listener) { return mModifiers; } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt new file mode 100644 index 000000000000..ac7d1f5ba452 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt @@ -0,0 +1,91 @@ +/* + * 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.display.brightness.clamper + +import android.os.PowerManager +import android.os.UserHandle +import android.provider.Settings +import android.testing.TestableContext +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.display.brightness.BrightnessReason +import com.android.server.testutils.TestHandler +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock + +private const val userId = UserHandle.USER_CURRENT + +class BrightnessLowLuxModifierTest { + + private var mockClamperChangeListener = + mock<BrightnessClamperController.ClamperChangeListener>() + + val context = TestableContext( + InstrumentationRegistry.getInstrumentation().getContext()) + + private val testHandler = TestHandler(null) + private lateinit var modifier: BrightnessLowLuxModifier + + @Before + fun setUp() { + modifier = BrightnessLowLuxModifier(testHandler, mockClamperChangeListener, context) + testHandler.flush() + } + + @Test + fun testThrottlingBounds() { + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // true + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId) + modifier.recalculateLowerBound() + testHandler.flush() + assertThat(modifier.isActive).isTrue() + + // TODO: code currently returns MIN/MAX; update with lux values + assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN) + } + + @Test + fun testGetReason_UserSet() { + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId) + modifier.recalculateLowerBound() + testHandler.flush() + assertThat(modifier.isActive).isTrue() + + // Test restriction from user setting + assertThat(modifier.brightnessReason) + .isEqualTo(BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND) + } + + @Test + fun testGetReason_Lux() { + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId) + modifier.recalculateLowerBound() + testHandler.flush() + assertThat(modifier.isActive).isTrue() + + // Test restriction from lux setting + assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java new file mode 100644 index 000000000000..b363fd4cc7cb --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java @@ -0,0 +1,381 @@ +/* + * 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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.when; + +import android.media.projection.MediaProjectionInfo; +import android.media.projection.MediaProjectionManager; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.NotificationListenerService.RankingMap; +import android.service.notification.StatusBarNotification; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper.RunWithLooper; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.SensitiveContentPackages.PackageInfo; +import com.android.server.wm.WindowManagerInternal; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; +import java.util.Set; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class SensitiveContentProtectionManagerServiceTest { + private static final String NOTIFICATION_KEY_1 = "com.android.server.notification.TEST_KEY_1"; + private static final String NOTIFICATION_KEY_2 = "com.android.server.notification.TEST_KEY_2"; + + private static final String NOTIFICATION_PKG_1 = "com.android.server.notification.one"; + private static final String NOTIFICATION_PKG_2 = "com.android.server.notification.two"; + + private static final int NOTIFICATION_UID_1 = 5; + private static final int NOTIFICATION_UID_2 = 6; + + @Rule + public final TestableContext mContext = + new TestableContext(getInstrumentation().getTargetContext(), null); + + private SensitiveContentProtectionManagerService mSensitiveContentProtectionManagerService; + + @Captor + ArgumentCaptor<MediaProjectionManager.Callback> mMediaProjectionCallbackCaptor; + + @Mock + private MediaProjectionManager mProjectionManager; + + @Mock + private WindowManagerInternal mWindowManager; + + @Mock + private StatusBarNotification mNotification1; + + @Mock + private StatusBarNotification mNotification2; + + @Mock + private RankingMap mRankingMap; + + @Mock + private Ranking mSensitiveRanking; + + @Mock + private Ranking mNonSensitiveRanking; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mSensitiveContentProtectionManagerService = + new SensitiveContentProtectionManagerService(mContext); + + mSensitiveContentProtectionManagerService.mNotificationListener = + spy(mSensitiveContentProtectionManagerService.mNotificationListener); + + // Setup RankingMap and two possilbe rankings + when(mSensitiveRanking.hasSensitiveContent()).thenReturn(true); + when(mNonSensitiveRanking.hasSensitiveContent()).thenReturn(false); + doReturn(mRankingMap) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.init(mProjectionManager, mWindowManager); + + // Obtain useful mMediaProjectionCallback + verify(mProjectionManager).addCallback(mMediaProjectionCallbackCaptor.capture(), any()); + } + + @After + public void tearDown() { + mSensitiveContentProtectionManagerService.onDestroy(); + } + + private Set<PackageInfo> setupSensitiveNotification() { + // Setup Notification Values + when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); + when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1); + + when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2); + when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_2); + when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_1); + + StatusBarNotification[] mNotifications = + new StatusBarNotification[] {mNotification1, mNotification2}; + doReturn(mNotifications) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))) + .thenReturn(mSensitiveRanking); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) + .thenReturn(mNonSensitiveRanking); + + return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1)); + } + + private Set<PackageInfo> setupMultipleSensitiveNotificationsFromSamePackageAndUid() { + // Setup Notification Values + when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); + when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1); + + when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2); + when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_1); + + StatusBarNotification[] mNotifications = + new StatusBarNotification[] {mNotification1, mNotification2}; + doReturn(mNotifications) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))) + .thenReturn(mSensitiveRanking); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) + .thenReturn(mSensitiveRanking); + + return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1)); + } + + private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentPackage() { + // Setup Notification Values + when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); + when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1); + + when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2); + when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_2); + when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_1); + + StatusBarNotification[] mNotifications = + new StatusBarNotification[] {mNotification1, mNotification2}; + doReturn(mNotifications) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))) + .thenReturn(mSensitiveRanking); + 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)); + } + + private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentUid() { + // Setup Notification Values + when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); + when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1); + + when(mNotification2.getKey()).thenReturn(NOTIFICATION_KEY_2); + when(mNotification2.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification2.getUid()).thenReturn(NOTIFICATION_UID_2); + + StatusBarNotification[] mNotifications = + new StatusBarNotification[] {mNotification1, mNotification2}; + doReturn(mNotifications) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))) + .thenReturn(mSensitiveRanking); + 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)); + } + + private void setupNoSensitiveNotifications() { + // Setup Notification Values + when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); + when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); + when(mNotification1.getUid()).thenReturn(NOTIFICATION_UID_1); + + StatusBarNotification[] mNotifications = new StatusBarNotification[] {mNotification1}; + doReturn(mNotifications) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))) + .thenReturn(mNonSensitiveRanking); + } + + private void setupNoNotifications() { + // Setup Notification Values + StatusBarNotification[] mNotifications = new StatusBarNotification[] {}; + doReturn(mNotifications) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + } + + @Test + public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() { + Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + } + + @Test + public void mediaProjectionOnStart_noSensitiveNotifications_noBlockedPackages() { + setupNoSensitiveNotifications(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void mediaProjectionOnStart_noNotifications_noBlockedPackages() { + setupNoNotifications(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void mediaProjectionOnStart_multipleNotifications_setWmBlockedPackages() { + Set<PackageInfo> expectedBlockedPackages = + setupMultipleSensitiveNotificationsFromSamePackageAndUid(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + } + + @Test + public void mediaProjectionOnStart_multiplePackages_setWmBlockedPackages() { + Set<PackageInfo> expectedBlockedPackages = + setupMultipleSensitiveNotificationsFromDifferentPackage(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + } + + @Test + public void mediaProjectionOnStart_multipleUid_setWmBlockedPackages() { + Set<PackageInfo> expectedBlockedPackages = + setupMultipleSensitiveNotificationsFromDifferentUid(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + } + + @Test + public void mediaProjectionOnStop_onProjectionEnd_clearWmBlockedPackages() { + setupSensitiveNotification(); + + MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); + mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); + Mockito.reset(mWindowManager); + + mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void mediaProjectionOnStart_afterOnStop_onProjectionStart_setWmBlockedPackages() { + Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + + MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); + mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); + mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); + Mockito.reset(mWindowManager); + + mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + } + + @Test + public void mediaProjectionOnStart_getActiveNotificationsThrows_noBlockedPackages() { + doThrow(SecurityException.class) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void mediaProjectionOnStart_getCurrentRankingThrows_noBlockedPackages() { + doThrow(SecurityException.class) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void mediaProjectionOnStart_getCurrentRanking_nullRankingMap_noBlockedPackages() { + doReturn(null) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void mediaProjectionOnStart_getCurrentRanking_missingRanking_noBlockedPackages() { + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + + doReturn(mRankingMap) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + + verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index f386c3bf6a15..d876dae29798 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -80,10 +80,13 @@ import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.ApplicationExitInfo; import android.app.IApplicationThread; import android.app.IServiceConnection; import android.content.ComponentName; @@ -94,9 +97,11 @@ import android.content.pm.ServiceInfo; import android.os.Build; import android.os.IBinder; import android.os.PowerManagerInternal; +import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArrayMap; import android.util.SparseArray; @@ -107,7 +112,9 @@ import com.android.server.wm.WindowProcessController; import org.junit.After; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import java.io.File; @@ -148,12 +155,18 @@ public class MockingOomAdjusterTests { private static final String MOCKAPP5_PROCESSNAME = "test #5"; private static final String MOCKAPP5_PACKAGENAME = "com.android.test.test5"; private static final int MOCKAPP2_UID_OTHER = MOCKAPP2_UID + UserHandle.PER_USER_RANGE; + private static final int MOCKAPP_ISOLATED_UID = Process.FIRST_ISOLATED_UID + 321; + private static final String MOCKAPP_ISOLATED_PROCESSNAME = "isolated test #1"; + private static int sFirstCachedAdj = ProcessList.CACHED_APP_MIN_ADJ + ProcessList.CACHED_APP_IMPORTANCE_LEVELS; private static Context sContext; private static PackageManagerInternal sPackageManagerInternal; private static ActivityManagerService sService; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @SuppressWarnings("GuardedBy") @BeforeClass public static void setUpOnce() { @@ -220,6 +233,11 @@ public class MockingOomAdjusterTests { } } + @Before + public void setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_NEW_FGS_RESTRICTION_LOGIC); + } + @AfterClass public static void tearDownOnce() { LocalServices.removeServiceForTest(PackageManagerInternal.class); @@ -286,6 +304,20 @@ public class MockingOomAdjusterTests { } /** + * Run updateOomAdjPendingTargetsLocked(). + * - enqueues all provided processes to the pending list and lru before running + */ + @SuppressWarnings("GuardedBy") + private void updateOomAdjPending(ProcessRecord... apps) { + setProcessesToLru(apps); + for (ProcessRecord app : apps) { + sService.mOomAdjuster.enqueueOomAdjTargetLocked(app); + } + sService.mOomAdjuster.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_NONE); + sService.mProcessList.getLruProcessesLOSP().clear(); + } + + /** * Fix up the pointers in the {@link ProcessRecordNode#mApp}: * because we used the mokito spy objects all over the tests here, but the internal * pointers in the {@link ProcessRecordNode#mApp} actually point to the real object. @@ -519,6 +551,7 @@ public class MockingOomAdjusterTests { sService.mConstants.mShortFgsProcStateExtraWaitDuration = 200_000; ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService); + s.appInfo = new ApplicationInfo(); s.startRequested = true; s.isForeground = true; s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; @@ -561,6 +594,7 @@ public class MockingOomAdjusterTests { // SHORT_SERVICE, timed out already. s = ServiceRecord.newEmptyInstanceForTest(sService); + s.appInfo = new ApplicationInfo(); s.startRequested = true; s.isForeground = true; s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; @@ -1079,6 +1113,7 @@ public class MockingOomAdjusterTests { // In order to trick OomAdjuster to think it has a short-service, we need this logic. ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService); + s.appInfo = new ApplicationInfo(); s.startRequested = true; s.isForeground = true; s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; @@ -1109,6 +1144,7 @@ public class MockingOomAdjusterTests { // In order to trick OomAdjuster to think it has a short-service, we need this logic. ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService); + s.appInfo = new ApplicationInfo(); s.startRequested = true; s.isForeground = true; s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; @@ -1400,6 +1436,7 @@ public class MockingOomAdjusterTests { // In order to trick OomAdjuster to think it has a short-service, we need this logic. ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService); + s.appInfo = new ApplicationInfo(); s.startRequested = true; s.isForeground = true; s.foregroundServiceType = FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; @@ -2651,6 +2688,90 @@ public class MockingOomAdjusterTests { assertNotEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj()); } + @SuppressWarnings("GuardedBy") + @Test + public void testUpdateOomAdj_DoAll_Isolated_stopService() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID, + MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false)); + + setProcessesToLru(app); + ServiceRecord s = makeServiceRecord(app); + s.startRequested = true; + s.lastActivity = SystemClock.uptimeMillis(); + sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); + updateOomAdj(); + assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND); + + app.mServices.stopService(s); + updateOomAdj(); + // isolated process should be killed immediately after service stop. + verify(app).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testUpdateOomAdj_DoPending_Isolated_stopService() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID, + MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false)); + + ServiceRecord s = makeServiceRecord(app); + s.startRequested = true; + s.lastActivity = SystemClock.uptimeMillis(); + sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); + updateOomAdjPending(app); + assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND); + + app.mServices.stopService(s); + updateOomAdjPending(app); + // isolated process should be killed immediately after service stop. + verify(app).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testUpdateOomAdj_DoAll_Isolated_stopServiceWithEntryPoint() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID, + MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false)); + app.setIsolatedEntryPoint("test"); + + setProcessesToLru(app); + ServiceRecord s = makeServiceRecord(app); + s.startRequested = true; + s.lastActivity = SystemClock.uptimeMillis(); + sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); + updateOomAdj(); + assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND); + + app.mServices.stopService(s); + updateOomAdj(); + // isolated process with entry point should not be killed + verify(app, never()).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testUpdateOomAdj_DoPending_Isolated_stopServiceWithEntryPoint() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_ISOLATED_UID, + MOCKAPP_ISOLATED_PROCESSNAME, MOCKAPP_PACKAGENAME, false)); + app.setIsolatedEntryPoint("test"); + + ServiceRecord s = makeServiceRecord(app); + s.startRequested = true; + s.lastActivity = SystemClock.uptimeMillis(); + sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); + updateOomAdjPending(app); + assertProcStates(app, PROCESS_STATE_SERVICE, SERVICE_ADJ, SCHED_GROUP_BACKGROUND); + + app.mServices.stopService(s); + updateOomAdjPending(app); + // isolated process with entry point should not be killed + verify(app, never()).killLocked("isolated not needed", ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_ISOLATED_NOT_NEEDED, true); + } + private ProcessRecord makeDefaultProcessRecord(int pid, int uid, String processName, String packageName, boolean hasShownUi) { long now = SystemClock.uptimeMillis(); diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index fc2e5b0981e4..0703db2ab648 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -118,7 +118,8 @@ import java.util.function.Supplier; @Presubmit public class GameManagerServiceTests { @Mock MockContext mMockContext; - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.NULL_DEFAULT); private static final String TAG = "GameManagerServiceTests"; private static final String PACKAGE_NAME_INVALID = "com.android.app"; private static final int USER_ID_1 = 1001; 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 e5ecdc478df7..0403c64fc624 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -210,6 +210,9 @@ public class PackageArchiverTest { anyInt())).thenReturn(1); when(mInstallerService.getExistingDraftSessionId(anyInt(), any(), anyInt())).thenReturn( PackageInstaller.SessionInfo.INVALID_ID); + PackageInstallerSession session = mock(PackageInstallerSession.class); + when(mInstallerService.getSession(anyInt())).thenReturn(session); + when(session.getUnarchivalStatus()).thenReturn(PackageInstaller.UNARCHIVAL_STATUS_UNSET); doReturn(new ParceledListSlice<>(List.of(mock(ResolveInfo.class)))) .when(mPackageManagerService).queryIntentReceivers(any(), any(), any(), anyLong(), eq(mUserId)); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java index 60cedcfd6dd0..24e7242792fb 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java @@ -108,6 +108,20 @@ public class PackageMonitorCallbackHelperTest { } @Test + public void testPackageMonitorCallback_SuspendNoAccessCallbackNotCalled() throws Exception { + BiFunction<Integer, Bundle, Bundle> filterExtras = (callingUid, intentExtras) -> null; + + IRemoteCallback callback = createMockPackageMonitorCallback(); + mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */, + Binder.getCallingUid()); + mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGES_SUSPENDED, + FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0}, null /* instantUserIds */, + null /* broadcastAllowList */, mHandler, filterExtras); + + verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any()); + } + + @Test public void testPackageMonitorCallback_SuspendCallbackCalled() throws Exception { Bundle result = new Bundle(); result.putInt(Intent.EXTRA_UID, FAKE_PACKAGE_UID); 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 c42c735e3c9d..37ca09d9fa27 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -32,6 +32,7 @@ import static com.google.common.truth.Truth.assertThat; import android.Manifest; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.IActivityManager; import android.app.admin.DevicePolicyManager; import android.app.trust.ITrustListener; import android.app.trust.ITrustManager; @@ -45,7 +46,8 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; -import android.net.Uri; +import android.hardware.biometrics.BiometricManager; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -53,7 +55,12 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; +import android.security.Authorization; +import android.security.authorization.IKeystoreAuthorization; import android.service.trust.TrustAgentService; import android.testing.TestableContext; import android.view.IWindowManager; @@ -83,23 +90,34 @@ public class TrustManagerServiceTest { @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) + .spyStatic(ActivityManager.class) + .spyStatic(Authorization.class) .mockStatic(ServiceManager.class) .mockStatic(WindowManagerGlobal.class) .build(); @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule public final MockContext mMockContext = new MockContext( ApplicationProvider.getApplicationContext()); private static final String URI_SCHEME_PACKAGE = "package"; private static final int TEST_USER_ID = 50; + 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 }; + private static final long[] PROFILE_BIOMETRIC_SIDS = new long[] { 700L, 701L }; private final ArrayList<ResolveInfo> mTrustAgentResolveInfoList = new ArrayList<>(); private final ArrayList<ComponentName> mKnownTrustAgents = new ArrayList<>(); private final ArrayList<ComponentName> mEnabledTrustAgents = new ArrayList<>(); private @Mock ActivityManager mActivityManager; + private @Mock BiometricManager mBiometricManager; private @Mock DevicePolicyManager mDevicePolicyManager; + private @Mock IKeystoreAuthorization mKeystoreAuthorization; private @Mock LockPatternUtils mLockPatternUtils; private @Mock PackageManager mPackageManager; private @Mock UserManager mUserManager; @@ -113,6 +131,9 @@ public class TrustManagerServiceTest { @Before public void setUp() throws Exception { when(mActivityManager.isUserRunning(TEST_USER_ID)).thenReturn(true); + doReturn(mock(IActivityManager.class)).when(() -> ActivityManager.getService()); + + doReturn(mKeystoreAuthorization).when(() -> Authorization.getService()); when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager); when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true); @@ -146,6 +167,7 @@ public class TrustManagerServiceTest { when(mWindowManager.isKeyguardLocked()).thenReturn(true); mMockContext.addMockSystemService(ActivityManager.class, mActivityManager); + mMockContext.addMockSystemService(BiometricManager.class, mBiometricManager); mMockContext.setMockPackageManager(mPackageManager); mMockContext.addMockSystemService(UserManager.class, mUserManager); doReturn(mWindowManager).when(() -> WindowManagerGlobal.getWindowManagerService()); @@ -257,7 +279,7 @@ public class TrustManagerServiceTest { "com.android/.SystemTrustAgent"); addTrustAgent(newAgentComponentName, /* isSystemApp= */ true); - mMockContext.sendPackageChangedBroadcast(newAgentComponentName); + notifyPackageChanged(newAgentComponentName); assertThat(mEnabledTrustAgents).containsExactly(newAgentComponentName); assertThat(mKnownTrustAgents).containsExactly(newAgentComponentName); @@ -276,7 +298,7 @@ public class TrustManagerServiceTest { "com.android/.SystemTrustAgent"); addTrustAgent(newAgentComponentName, /* isSystemApp= */ true); - mMockContext.sendPackageChangedBroadcast(newAgentComponentName); + notifyPackageChanged(newAgentComponentName); assertThat(mEnabledTrustAgents).containsExactly(defaultTrustAgent); assertThat(mKnownTrustAgents).containsExactly(defaultTrustAgent, newAgentComponentName); @@ -289,7 +311,7 @@ public class TrustManagerServiceTest { "com.user/.UserTrustAgent"); addTrustAgent(newAgentComponentName, /* isSystemApp= */ false); - mMockContext.sendPackageChangedBroadcast(newAgentComponentName); + notifyPackageChanged(newAgentComponentName); assertThat(mEnabledTrustAgents).isEmpty(); assertThat(mKnownTrustAgents).containsExactly(newAgentComponentName); @@ -307,7 +329,7 @@ public class TrustManagerServiceTest { // Simulate user turning off systemTrustAgent2 mLockPatternUtils.setEnabledTrustAgents(List.of(systemTrustAgent1), TEST_USER_ID); - mMockContext.sendPackageChangedBroadcast(systemTrustAgent2); + notifyPackageChanged(systemTrustAgent2); assertThat(mEnabledTrustAgents).containsExactly(systemTrustAgent1); } @@ -322,6 +344,73 @@ public class TrustManagerServiceTest { verify(trustListener).onEnabledTrustAgentsChanged(TEST_USER_ID); } + // Tests that when the device is locked for a managed profile with a *unified* challenge, the + // device locked notification that is sent to Keystore contains the biometric SIDs of the parent + // user, not the profile. This matches the authentication that is needed to unlock the device + // for the profile again. + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2) + public void testLockDeviceForManagedProfileWithUnifiedChallenge_usesParentBiometricSids() + throws Exception { + setupMocksForProfile(/* unifiedChallenge= */ true); + + when(mWindowManager.isKeyguardLocked()).thenReturn(false); + mTrustManager.reportKeyguardShowingChanged(); + verify(mKeystoreAuthorization).onDeviceUnlocked(PARENT_USER_ID, null); + verify(mKeystoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null); + + when(mWindowManager.isKeyguardLocked()).thenReturn(true); + mTrustManager.reportKeyguardShowingChanged(); + verify(mKeystoreAuthorization) + .onDeviceLocked(eq(PARENT_USER_ID), eq(PARENT_BIOMETRIC_SIDS)); + verify(mKeystoreAuthorization) + .onDeviceLocked(eq(PROFILE_USER_ID), eq(PARENT_BIOMETRIC_SIDS)); + } + + // Tests that when the device is locked for a managed profile with a *separate* challenge, the + // device locked notification that is sent to Keystore contains the biometric SIDs of the + // profile itself. This matches the authentication that is needed to unlock the device for the + // profile again. + @Test + public void testLockDeviceForManagedProfileWithSeparateChallenge_usesProfileBiometricSids() + throws Exception { + setupMocksForProfile(/* unifiedChallenge= */ false); + + mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, false); + verify(mKeystoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null); + + mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, true); + verify(mKeystoreAuthorization) + .onDeviceLocked(eq(PROFILE_USER_ID), eq(PROFILE_BIOMETRIC_SIDS)); + } + + private void setupMocksForProfile(boolean unifiedChallenge) { + UserInfo parent = new UserInfo(PARENT_USER_ID, "parent", UserInfo.FLAG_FULL); + UserInfo profile = new UserInfo(PROFILE_USER_ID, "profile", UserInfo.FLAG_MANAGED_PROFILE); + when(mUserManager.getAliveUsers()).thenReturn(List.of(parent, profile)); + when(mUserManager.getUserInfo(PARENT_USER_ID)).thenReturn(parent); + when(mUserManager.getUserInfo(PROFILE_USER_ID)).thenReturn(profile); + when(mUserManager.getProfileParent(PROFILE_USER_ID)).thenReturn(parent); + when(mUserManager.getEnabledProfileIds(PARENT_USER_ID)) + .thenReturn(new int[] { PROFILE_USER_ID }); + + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + when(mLockPatternUtils.isProfileWithUnifiedChallenge(PROFILE_USER_ID)) + .thenReturn(unifiedChallenge); + when(mLockPatternUtils.isManagedProfileWithUnifiedChallenge(PROFILE_USER_ID)) + .thenReturn(unifiedChallenge); + when(mLockPatternUtils.isSeparateProfileChallengeEnabled(PROFILE_USER_ID)) + .thenReturn(!unifiedChallenge); + + when(mBiometricManager.getAuthenticatorIds(PARENT_USER_ID)) + .thenReturn(PARENT_BIOMETRIC_SIDS); + when(mBiometricManager.getAuthenticatorIds(PROFILE_USER_ID)) + .thenReturn(PROFILE_BIOMETRIC_SIDS); + + bootService(); + mService.onUserSwitching(null, new SystemService.TargetUser(parent)); + } + private void addTrustAgent(ComponentName agentComponentName, boolean isSystemApp) { ApplicationInfo applicationInfo = new ApplicationInfo(); if (isSystemApp) { @@ -350,11 +439,16 @@ public class TrustManagerServiceTest { permission, PackageManager.PERMISSION_GRANTED); } + private void notifyPackageChanged(ComponentName changedComponent) { + mService.mPackageMonitor.onPackageChanged( + changedComponent.getPackageName(), + UserHandle.of(TEST_USER_ID).getUid(1234), + new String[] { changedComponent.getClassName() }); + } + /** A mock Context that allows the test process to send protected broadcasts. */ private static final class MockContext extends TestableContext { - private final ArrayList<BroadcastReceiver> mPackageChangedBroadcastReceivers = - new ArrayList<>(); private final ArrayList<BroadcastReceiver> mUserStartedBroadcastReceivers = new ArrayList<>(); @@ -368,9 +462,6 @@ public class TrustManagerServiceTest { UserHandle user, IntentFilter filter, @Nullable String broadcastPermission, @Nullable Handler scheduler) { - if (filter.hasAction(Intent.ACTION_PACKAGE_CHANGED)) { - mPackageChangedBroadcastReceivers.add(receiver); - } if (filter.hasAction(Intent.ACTION_USER_STARTED)) { mUserStartedBroadcastReceivers.add(receiver); } @@ -378,18 +469,9 @@ public class TrustManagerServiceTest { scheduler); } - void sendPackageChangedBroadcast(ComponentName changedComponent) { - Intent intent = new Intent( - Intent.ACTION_PACKAGE_CHANGED, - Uri.fromParts(URI_SCHEME_PACKAGE, - changedComponent.getPackageName(), /* fragment= */ null)) - .putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, - new String[]{changedComponent.getClassName()}) - .putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID) - .putExtra(Intent.EXTRA_UID, UserHandle.of(TEST_USER_ID).getUid(1234)); - for (BroadcastReceiver receiver : mPackageChangedBroadcastReceivers) { - receiver.onReceive(this, intent); - } + @Override + public void sendBroadcastAsUser(Intent intent, UserHandle user, + @Nullable String receiverPermission, @Nullable Bundle options) { } void sendUserStartedBroadcast() { diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index 05e0e8f4a4f7..654d7a8de168 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -11,6 +11,7 @@ filegroup { "src/com/android/server/power/stats/MultiStateStatsTest.java", "src/com/android/server/power/stats/PowerStatsAggregatorTest.java", "src/com/android/server/power/stats/PowerStatsCollectorTest.java", + "src/com/android/server/power/stats/PowerStatsExporterTest.java", "src/com/android/server/power/stats/PowerStatsSchedulerTest.java", "src/com/android/server/power/stats/PowerStatsStoreTest.java", "src/com/android/server/power/stats/PowerStatsUidResolverTest.java", @@ -83,6 +84,9 @@ android_ravenwood_test { ], srcs: [ ":power_stats_ravenwood_tests", + + "src/com/android/server/power/stats/BatteryUsageStatsRule.java", + "src/com/android/server/power/stats/MockBatteryStatsImpl.java", "src/com/android/server/power/stats/MockClock.java", ], auto_gen_config: true, diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java index 4ea0805ffaa7..3f058a2348c3 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java @@ -37,12 +37,12 @@ public class BatteryChargeCalculatorTest { private static final double PRECISION = 0.00001; @Rule - public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule(); + public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() + .setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0); @Test public void testDischargeTotals() { // Nominal battery capacity should be ignored - mStatsRule.setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 1234.0); final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); @@ -84,8 +84,6 @@ public class BatteryChargeCalculatorTest { @Test public void testDischargeTotals_chargeUahUnavailable() { - mStatsRule.setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0); - final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100, diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index e61dd0b41423..ca162e0b46e1 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -18,11 +18,11 @@ package com.android.server.power.stats; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.annotation.XmlRes; -import android.content.Context; import android.net.NetworkStats; import android.os.BatteryConsumer; import android.os.BatteryStats; @@ -54,12 +54,11 @@ public class BatteryUsageStatsRule implements TestRule { .powerProfileModeledOnly() .includePowerModels() .build(); - private final Context mContext; private final PowerProfile mPowerProfile; private final MockClock mMockClock = new MockClock(); private final MockBatteryStatsImpl mBatteryStats; - private final Handler mHandler; + private Handler mHandler; private BatteryUsageStats mBatteryUsageStats; private boolean mScreenOn; @@ -76,11 +75,8 @@ public class BatteryUsageStatsRule implements TestRule { } public BatteryUsageStatsRule(long currentTime, File historyDir) { - HandlerThread bgThread = new HandlerThread("bg thread"); - bgThread.start(); - mHandler = new Handler(bgThread.getLooper()); - mContext = InstrumentationRegistry.getContext(); - mPowerProfile = spy(new PowerProfile(mContext, true /* forTest */)); + mHandler = mock(Handler.class); + mPowerProfile = spy(new PowerProfile()); mMockClock.currentTime = currentTime; mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir, mHandler); mBatteryStats.setPowerProfile(mPowerProfile); @@ -103,7 +99,7 @@ public class BatteryUsageStatsRule implements TestRule { } public BatteryUsageStatsRule setTestPowerProfile(@XmlRes int xmlId) { - mPowerProfile.forceInitForTesting(mContext, xmlId); + mPowerProfile.forceInitForTesting(InstrumentationRegistry.getContext(), xmlId); return this; } @@ -222,13 +218,17 @@ public class BatteryUsageStatsRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { - noteOnBattery(); + before(); base.evaluate(); } }; } - private void noteOnBattery() { + private void before() { + HandlerThread bgThread = new HandlerThread("bg thread"); + bgThread.start(); + mHandler = new Handler(bgThread.getLooper()); + mBatteryStats.setHandler(mHandler); mBatteryStats.setOnBatteryInternal(true); mBatteryStats.getOnBatteryTimeBase().setRunning(true, 0, 0); mBatteryStats.getOnBatteryScreenOffTimeBase().setRunning(!mScreenOn, 0, 0); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java index fb71ac83a8d9..78c4bac14a74 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java @@ -271,6 +271,10 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { public void writeSyncLocked() { } + public void setHandler(Handler handler) { + mHandler = handler; + } + public static class DummyExternalStatsSync implements ExternalStatsSync { public int flags = 0; @@ -315,4 +319,3 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { } } } - diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java index 3c482625b038..3560a2620c8f 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java @@ -16,8 +16,6 @@ package com.android.server.power.stats; -import static androidx.test.InstrumentationRegistry.getContext; - import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -34,6 +32,7 @@ import android.os.Message; import android.os.Parcel; import android.os.PersistableBundle; import android.os.UidBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.runner.AndroidJUnit4; @@ -48,6 +47,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.List; @RunWith(AndroidJUnit4.class) @@ -57,7 +58,12 @@ public class PowerStatsExporterTest { private static final int APP_UID2 = 84; private static final double TOLERANCE = 0.01; - @Rule + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Rule(order = 1) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720) .setCpuScalingPolicy(0, new int[]{0}, new int[]{100}) @@ -75,8 +81,8 @@ public class PowerStatsExporterTest { private PowerStats.Descriptor mPowerStatsDescriptor; @Before - public void setup() { - File storeDirectory = new File(getContext().getCacheDir(), getClass().getSimpleName()); + public void setup() throws IOException { + File storeDirectory = Files.createTempDirectory("PowerStatsExporterTest").toFile(); clearDirectory(storeDirectory); AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig(); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 9b8419021c5c..00450267ee79 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -215,6 +215,16 @@ java_library { } java_library { + name: "mockito-test-utils", + srcs: [ + "utils-mockito/**/*.kt", + ], + static_libs: [ + "mockito-target-minus-junit4", + ], +} + +java_library { name: "servicestests-utils-mockito-extended", srcs: [ "utils/**/*.java", diff --git a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java b/services/tests/servicestests/src/com/android/server/BootReceiverTest.java new file mode 100644 index 000000000000..523c5c060cf5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/BootReceiverTest.java @@ -0,0 +1,97 @@ +/* + * 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/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 77b1455a2ecc..26934d8e5d6e 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -763,8 +763,7 @@ public class UserControllerTest { mUserController.startUser(TEST_USER_ID, USER_START_MODE_BACKGROUND); - verify(mInjector.mStorageManagerMock, never()) - .unlockCeStorage(eq(TEST_USER_ID), anyInt(), any()); + verify(mInjector.mStorageManagerMock, never()).unlockCeStorage(eq(TEST_USER_ID), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index 89299002a227..f7480dea780b 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -44,12 +44,18 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.fingerprint.IFingerprint; +import android.hardware.biometrics.fingerprint.ISession; import android.hardware.fingerprint.Fingerprint; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -60,6 +66,7 @@ import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.nano.BiometricSchedulerProto; @@ -95,14 +102,39 @@ public class BiometricSchedulerTest { @Rule public final TestableContext mContext = new TestableContext( InstrumentationRegistry.getContext(), null); - private BiometricScheduler mScheduler; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private BiometricScheduler<IFingerprint, ISession> mScheduler; private IBinder mToken; + private int mCurrentUserId = UserHandle.USER_SYSTEM; + private boolean mShouldFailStopUser = false; + private final List<Integer> mStartedUsers = new ArrayList<>(); + private final StartUserClient.UserStartedCallback<ISession> mUserStartedCallback = + (newUserId, newUser, halInterfaceVersion) -> { + mStartedUsers.add(newUserId); + mCurrentUserId = newUserId; + }; + private int mUsersStoppedCount = 0; + private final StopUserClient.UserStoppedCallback mUserStoppedCallback = + () -> { + mUsersStoppedCount++; + mCurrentUserId = UserHandle.USER_NULL; + }; + private boolean mStartOperationsFinish = true; + private int mStartUserClientCount = 0; @Mock private IBiometricService mBiometricService; @Mock private BiometricContext mBiometricContext; @Mock private AuthSessionCoordinator mAuthSessionCoordinator; + @Mock + private BiometricLogger mBiometricLogger; + @Mock + private ISession mSession; + @Mock + private IFingerprint mFingerprint; @Before public void setUp() { @@ -111,9 +143,39 @@ public class BiometricSchedulerTest { when(mAuthSessionCoordinator.getLockoutStateFor(anyInt(), anyInt())).thenReturn( BIOMETRIC_SUCCESS); when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator); - mScheduler = new BiometricScheduler(TAG, new Handler(TestableLooper.get(this).getLooper()), - BiometricScheduler.SENSOR_TYPE_UNKNOWN, null /* gestureAvailabilityTracker */, - mBiometricService, LOG_NUM_RECENT_OPERATIONS); + if (Flags.deHidl()) { + mScheduler = new BiometricScheduler<>( + new Handler(TestableLooper.get(this).getLooper()), + BiometricScheduler.SENSOR_TYPE_UNKNOWN, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + LOG_NUM_RECENT_OPERATIONS, + () -> mCurrentUserId, + new UserSwitchProvider<IFingerprint, ISession>() { + @NonNull + @Override + public StopUserClient<ISession> getStopUserClient(int userId) { + return new TestStopUserClient(mContext, () -> mSession, mToken, userId, + TEST_SENSOR_ID, mBiometricLogger, mBiometricContext, + mUserStoppedCallback, () -> mShouldFailStopUser); + } + + @NonNull + @Override + public StartUserClient<IFingerprint, ISession> getStartUserClient( + int newUserId) { + mStartUserClientCount++; + return new TestStartUserClient(mContext, () -> mFingerprint, mToken, + newUserId, TEST_SENSOR_ID, mBiometricLogger, mBiometricContext, + mUserStartedCallback, mStartOperationsFinish); + } + }); + } else { + mScheduler = new BiometricScheduler<>( + new Handler(TestableLooper.get(this).getLooper()), + BiometricScheduler.SENSOR_TYPE_UNKNOWN, null /* gestureAvailabilityTracker */, + mBiometricService, LOG_NUM_RECENT_OPERATIONS); + } } @Test @@ -479,6 +541,7 @@ public class BiometricSchedulerTest { final boolean isEnroll = client instanceof TestEnrollClient; mScheduler.scheduleClientMonitor(client); + waitForIdle(); if (started) { mScheduler.startPreparedClient(client.getCookie()); } @@ -789,6 +852,172 @@ public class BiometricSchedulerTest { assertEquals(1, client1.getFingerprints().size()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testScheduleOperation_whenNoUser() { + mCurrentUserId = UserHandle.USER_NULL; + + final BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(0); + + mScheduler.scheduleClientMonitor(nextClient); + waitForIdle(); + + assertThat(mUsersStoppedCount).isEqualTo(0); + assertThat(mStartedUsers).containsExactly(0); + verify(nextClient).start(any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testScheduleOperation_whenNoUser_notStarted() { + mCurrentUserId = UserHandle.USER_NULL; + mStartOperationsFinish = false; + + final BaseClientMonitor[] nextClients = new BaseClientMonitor[]{ + mock(BaseClientMonitor.class), + mock(BaseClientMonitor.class), + mock(BaseClientMonitor.class) + }; + for (BaseClientMonitor client : nextClients) { + when(client.getTargetUserId()).thenReturn(5); + mScheduler.scheduleClientMonitor(client); + waitForIdle(); + } + + assertThat(mUsersStoppedCount).isEqualTo(0); + assertThat(mStartedUsers).isEmpty(); + assertThat(mStartUserClientCount).isEqualTo(1); + for (BaseClientMonitor client : nextClients) { + verify(client, never()).start(any()); + } + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testScheduleOperation_whenNoUser_notStarted_andReset() { + mCurrentUserId = UserHandle.USER_NULL; + mStartOperationsFinish = false; + + final BaseClientMonitor client = mock(BaseClientMonitor.class); + + when(client.getTargetUserId()).thenReturn(5); + + mScheduler.scheduleClientMonitor(client); + waitForIdle(); + + final TestStartUserClient startUserClient = + (TestStartUserClient) mScheduler.mCurrentOperation.getClientMonitor(); + mScheduler.reset(); + + assertThat(mScheduler.mCurrentOperation).isNull(); + + final BiometricSchedulerOperation fakeOperation = new BiometricSchedulerOperation( + mock(BaseClientMonitor.class), new ClientMonitorCallback() {}); + mScheduler.mCurrentOperation = fakeOperation; + startUserClient.mCallback.onClientFinished(startUserClient, true); + + assertThat(fakeOperation).isSameInstanceAs(mScheduler.mCurrentOperation); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testScheduleOperation_whenSameUser() { + mCurrentUserId = 10; + + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(mCurrentUserId); + + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + + verify(nextClient).start(any()); + assertThat(mUsersStoppedCount).isEqualTo(0); + assertThat(mStartedUsers).isEmpty(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testScheduleOperation_whenDifferentUser() { + mCurrentUserId = 10; + + final int nextUserId = 11; + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(nextUserId); + + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + assertThat(mUsersStoppedCount).isEqualTo(1); + + waitForIdle(); + assertThat(mStartedUsers).containsExactly(nextUserId); + + waitForIdle(); + verify(nextClient).start(any()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testStartUser_alwaysStartsNextOperation() { + mCurrentUserId = UserHandle.USER_NULL; + + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(10); + + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + verify(nextClient).start(any()); + + // finish first operation + mScheduler.getInternalCallback().onClientFinished(nextClient, true /* success */); + waitForIdle(); + + // schedule second operation but swap out the current operation + // before it runs so that it's not current when it's completion callback runs + nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(11); + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + verify(nextClient).start(any()); + assertThat(mStartedUsers).containsExactly(10, 11).inOrder(); + assertThat(mUsersStoppedCount).isEqualTo(1); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL) + public void testStartUser_failsClearsStopUserClient() { + mCurrentUserId = UserHandle.USER_NULL; + + // When a stop user client fails, check that mStopUserClient + // is set to null to prevent the scheduler from getting stuck. + BaseClientMonitor nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(10); + + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + verify(nextClient).start(any()); + + // finish first operation + mScheduler.getInternalCallback().onClientFinished(nextClient, true /* success */); + waitForIdle(); + + // schedule second operation but swap out the current operation + // before it runs so that it's not current when it's completion callback runs + nextClient = mock(BaseClientMonitor.class); + when(nextClient.getTargetUserId()).thenReturn(11); + mShouldFailStopUser = true; + mScheduler.scheduleClientMonitor(nextClient); + + waitForIdle(); + assertThat(mStartedUsers).containsExactly(10, 11).inOrder(); + assertThat(mUsersStoppedCount).isEqualTo(0); + assertThat(mScheduler.getStopUserClient()).isEqualTo(null); + } private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception { return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer)); @@ -1069,4 +1298,82 @@ public class BiometricSchedulerTest { return mFingerprints; } } + + private interface StopUserClientShouldFail { + boolean shouldFail(); + } + + private class TestStopUserClient extends StopUserClient<ISession> { + private StopUserClientShouldFail mShouldFailClient; + TestStopUserClient(@NonNull Context context, + @NonNull Supplier<ISession> lazyDaemon, @Nullable IBinder token, int userId, + int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, + @NonNull UserStoppedCallback callback, StopUserClientShouldFail shouldFail) { + super(context, lazyDaemon, token, userId, sensorId, logger, biometricContext, callback); + mShouldFailClient = shouldFail; + } + + @Override + protected void startHalOperation() { + + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + if (mShouldFailClient.shouldFail()) { + getCallback().onClientFinished(this, false /* success */); + // When the above fails, it means that the HAL has died, in this case we + // need to ensure the UserSwitchCallback correctly returns the NULL user handle. + mCurrentUserId = UserHandle.USER_NULL; + } else { + onUserStopped(); + } + } + + @Override + public void unableToStart() { + + } + } + + private static class TestStartUserClient extends StartUserClient<IFingerprint, ISession> { + + @Mock + private ISession mSession; + private final boolean mShouldFinish; + ClientMonitorCallback mCallback; + + TestStartUserClient(@NonNull Context context, + @NonNull Supplier<IFingerprint> lazyDaemon, @Nullable IBinder token, int userId, + int sensorId, @NonNull BiometricLogger logger, + @NonNull BiometricContext biometricContext, + @NonNull UserStartedCallback<ISession> callback, boolean shouldFinish) { + super(context, lazyDaemon, token, userId, sensorId, logger, biometricContext, callback); + mShouldFinish = shouldFinish; + } + + @Override + protected void startHalOperation() { + + } + + @Override + public void start(@NonNull ClientMonitorCallback callback) { + super.start(callback); + + mCallback = callback; + if (mShouldFinish) { + mUserStartedCallback.onUserStarted( + getTargetUserId(), mSession, 1 /* halInterfaceVersion */); + callback.onClientFinished(this, true /* success */); + } + } + + @Override + public void unableToStart() { + + } + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java index 8b1a2915820a..772ec8b73393 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java @@ -36,8 +36,10 @@ import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.SensorProps; import android.hardware.face.HidlFaceSensorConfig; +import android.os.Handler; import android.os.RemoteException; import android.os.UserManager; +import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -88,14 +90,11 @@ public class FaceProviderTest { @Mock private BiometricStateCallback mBiometricStateCallback; + private final TestLooper mLooper = new TestLooper(); private SensorProps[] mSensorProps; private LockoutResetDispatcher mLockoutResetDispatcher; private FaceProvider mFaceProvider; - private static void waitForIdle() { - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - } - @Before public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); @@ -121,7 +120,8 @@ public class FaceProviderTest { mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback, mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext, - mDaemon, false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */); + mDaemon, new Handler(mLooper.getLooper()), + false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */); } @Test @@ -156,6 +156,7 @@ public class FaceProviderTest { mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback, hidlFaceSensorConfig, TAG, mLockoutResetDispatcher, mBiometricContext, mDaemon, + new Handler(mLooper.getLooper()), true /* resetLockoutRequiresChallenge */, true /* testHalEnabled */); @@ -210,4 +211,12 @@ public class FaceProviderTest { assertEquals(0, scheduler.getCurrentPendingCount()); } } + + private void waitForIdle() { + if (Flags.deHidl()) { + mLooper.dispatchAll(); + } else { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java index e7f7195588ff..fe9cd4353603 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.common.CommonProps; +import android.hardware.biometrics.face.IFace; import android.hardware.biometrics.face.ISession; import android.hardware.biometrics.face.SensorProps; import android.hardware.face.FaceSensorPropertiesInternal; @@ -40,6 +41,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; @@ -49,6 +51,7 @@ import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.UserSwitchProvider; import org.junit.Before; import org.junit.Test; @@ -74,6 +77,8 @@ public class SensorTest { @Mock private UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback; @Mock + private UserSwitchProvider<IFace, ISession> mUserSwitchProvider; + @Mock private AidlResponseHandler.HardwareUnavailableCallback mHardwareUnavailableCallback; @Mock private LockoutResetDispatcher mLockoutResetDispatcher; @@ -84,16 +89,16 @@ public class SensorTest { @Mock private AuthSessionCoordinator mAuthSessionCoordinator; @Mock - FaceProvider mFaceProvider; + private FaceProvider mFaceProvider; @Mock - BaseClientMonitor mClientMonitor; + private BaseClientMonitor mClientMonitor; @Mock - AidlSession mCurrentSession; + private AidlSession mCurrentSession; private final TestLooper mLooper = new TestLooper(); private final LockoutCache mLockoutCache = new LockoutCache(); - private UserAwareBiometricScheduler mScheduler; + private BiometricScheduler<IFace, ISession> mScheduler; private AidlResponseHandler mHalCallback; @Before @@ -101,16 +106,26 @@ public class SensorTest { MockitoAnnotations.initMocks(this); when(mContext.getSystemService(Context.BIOMETRIC_SERVICE)).thenReturn(mBiometricService); - when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator); - mScheduler = new UserAwareBiometricScheduler(TAG, - new Handler(mLooper.getLooper()), - BiometricScheduler.SENSOR_TYPE_FACE, - null /* gestureAvailabilityDispatcher */, - mBiometricService, - () -> USER_ID, - mUserSwitchCallback); + if (Flags.deHidl()) { + mScheduler = new BiometricScheduler<>( + new Handler(mLooper.getLooper()), + BiometricScheduler.SENSOR_TYPE_FACE, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + 2 /* recentOperationsLimit */, + () -> USER_ID, + mUserSwitchProvider); + } else { + mScheduler = new UserAwareBiometricScheduler<>(TAG, + new Handler(mLooper.getLooper()), + BiometricScheduler.SENSOR_TYPE_FACE, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + () -> USER_ID, + mUserSwitchCallback); + } mHalCallback = new AidlResponseHandler(mContext, mScheduler, SENSOR_ID, USER_ID, mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator, mHardwareUnavailableCallback); @@ -146,18 +161,8 @@ public class SensorTest { @Test public void onBinderDied_noErrorOnNullClient() { mLooper.dispatchAll(); - - final SensorProps sensorProps = new SensorProps(); - sensorProps.commonProps = new CommonProps(); - sensorProps.commonProps.sensorId = 1; - final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( - sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength, - sensorProps.commonProps.maxEnrollmentsPerUser, null /* componentInfo */, - sensorProps.sensorType, sensorProps.supportsDetectInteraction, - sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */); - final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, - null /* handler */, internalProp, mLockoutResetDispatcher, mBiometricContext); - sensor.init(mLockoutResetDispatcher, mFaceProvider); + final Sensor sensor = getSensor(); + mScheduler = sensor.getScheduler(); mScheduler.reset(); assertNull(mScheduler.getCurrentClient()); @@ -175,18 +180,8 @@ public class SensorTest { when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID); when(mClientMonitor.isInterruptable()).thenReturn(false); - final SensorProps sensorProps = new SensorProps(); - sensorProps.commonProps = new CommonProps(); - sensorProps.commonProps.sensorId = 1; - final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( - sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength, - sensorProps.commonProps.maxEnrollmentsPerUser, null /* componentInfo */, - sensorProps.sensorType, sensorProps.supportsDetectInteraction, - sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */); - final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null, - internalProp, mLockoutResetDispatcher, mBiometricContext, mCurrentSession); - sensor.init(mLockoutResetDispatcher, mFaceProvider); - mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler(); + final Sensor sensor = getSensor(); + mScheduler = sensor.getScheduler(); sensor.mCurrentSession = new AidlSession(0, mock(ISession.class), USER_ID, mHalCallback); @@ -206,4 +201,20 @@ public class SensorTest { verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID)); verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); } + + private Sensor getSensor() { + final SensorProps sensorProps = new SensorProps(); + sensorProps.commonProps = new CommonProps(); + sensorProps.commonProps.sensorId = 1; + final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal( + sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength, + sensorProps.commonProps.maxEnrollmentsPerUser, null /* componentInfo */, + sensorProps.sensorType, sensorProps.supportsDetectInteraction, + sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */); + final Sensor sensor = new Sensor(mFaceProvider, mContext, + null /* handler */, internalProp, mBiometricContext); + sensor.init(mLockoutResetDispatcher, mFaceProvider); + + return sensor; + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java index 4e43332ab52c..940fe69925b5 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java @@ -45,7 +45,6 @@ import androidx.test.core.app.ApplicationProvider; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; -import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricUtils; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; @@ -88,9 +87,9 @@ public class HidlToAidlSensorAdapterTest { @Mock private IBiometricsFace mDaemon; @Mock - AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + private AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; @Mock - BiometricUtils<Face> mBiometricUtils; + private BiometricUtils<Face> mBiometricUtils; private final TestLooper mLooper = new TestLooper(); private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter; @@ -118,20 +117,14 @@ public class HidlToAidlSensorAdapterTest { mContext.getOrCreateTestableResources(); final String config = String.format("%d:8:15", SENSOR_ID); - final BiometricScheduler scheduler = new BiometricScheduler(TAG, - new Handler(mLooper.getLooper()), - BiometricScheduler.SENSOR_TYPE_FACE, - null /* gestureAvailabilityTracker */, - mBiometricService, 10 /* recentOperationsLimit */); final HidlFaceSensorConfig faceSensorConfig = new HidlFaceSensorConfig(); faceSensorConfig.parse(config, mContext); - mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG, mFaceProvider, + mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(mFaceProvider, mContext, new Handler(mLooper.getLooper()), faceSensorConfig, mLockoutResetDispatcherForSensor, mBiometricContext, false /* resetLockoutRequiresChallenge */, mInternalCleanupAndGetFeatureRunnable, mAuthSessionCoordinator, mDaemon, mAidlResponseHandlerCallback); mHidlToAidlSensorAdapter.init(mLockoutResetDispatcherForSensor, mFaceProvider); - mHidlToAidlSensorAdapter.setScheduler(scheduler); mHidlToAidlSensorAdapter.handleUserChanged(USER_ID); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java index bf5986c1d0f3..258be573d005 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java @@ -38,8 +38,10 @@ import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.SensorLocation; import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.fingerprint.HidlFingerprintSensorConfig; +import android.os.Handler; import android.os.RemoteException; import android.os.UserManager; +import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -92,14 +94,12 @@ public class FingerprintProviderTest { @Mock private BiometricContext mBiometricContext; + private final TestLooper mLooper = new TestLooper(); + private SensorProps[] mSensorProps; private LockoutResetDispatcher mLockoutResetDispatcher; private FingerprintProvider mFingerprintProvider; - private static void waitForIdle() { - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - } - @Before public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); @@ -126,7 +126,8 @@ public class FingerprintProviderTest { mFingerprintProvider = new FingerprintProvider(mContext, mBiometricStateCallback, mAuthenticationStateListeners, mSensorProps, TAG, mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext, - mDaemon, false /* resetLockoutRequiresHardwareAuthToken */, + mDaemon, new Handler(mLooper.getLooper()), + false /* resetLockoutRequiresHardwareAuthToken */, true /* testHalEnabled */); } @@ -159,6 +160,7 @@ public class FingerprintProviderTest { mBiometricStateCallback, mAuthenticationStateListeners, hidlFingerprintSensorConfigs, TAG, mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext, mDaemon, + new Handler(mLooper.getLooper()), false /* resetLockoutRequiresHardwareAuthToken */, true /* testHalEnabled */); @@ -215,4 +217,12 @@ public class FingerprintProviderTest { assertEquals(0, scheduler.getCurrentPendingCount()); } } + + private void waitForIdle() { + if (Flags.deHidl()) { + mLooper.dispatchAll(); + } else { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java index 126a05e12628..b4c2ee8f89bf 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java @@ -32,14 +32,17 @@ import android.content.Context; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.common.CommonProps; import android.hardware.biometrics.face.SensorProps; +import android.hardware.biometrics.fingerprint.IFingerprint; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Handler; +import android.os.HandlerThread; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; @@ -49,6 +52,7 @@ import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; +import com.android.server.biometrics.sensors.UserSwitchProvider; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import org.junit.Before; @@ -75,6 +79,8 @@ public class SensorTest { @Mock private UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback; @Mock + private UserSwitchProvider<IFingerprint, ISession> mUserSwitchProvider; + @Mock private AidlResponseHandler.HardwareUnavailableCallback mHardwareUnavailableCallback; @Mock private LockoutResetDispatcher mLockoutResetDispatcher; @@ -92,11 +98,13 @@ public class SensorTest { private AidlSession mCurrentSession; @Mock private BaseClientMonitor mClientMonitor; + @Mock + private HandlerThread mThread; private final TestLooper mLooper = new TestLooper(); private final LockoutCache mLockoutCache = new LockoutCache(); - private BiometricScheduler mScheduler; + private BiometricScheduler<IFingerprint, ISession> mScheduler; private AidlResponseHandler mHalCallback; @Before @@ -105,14 +113,26 @@ public class SensorTest { when(mContext.getSystemService(Context.BIOMETRIC_SERVICE)).thenReturn(mBiometricService); when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator); - - mScheduler = new UserAwareBiometricScheduler(TAG, - new Handler(mLooper.getLooper()), - BiometricScheduler.SENSOR_TYPE_FP_OTHER, - null /* gestureAvailabilityDispatcher */, - mBiometricService, - () -> USER_ID, - mUserSwitchCallback); + when(mThread.getLooper()).thenReturn(mLooper.getLooper()); + + if (Flags.deHidl()) { + mScheduler = new BiometricScheduler<>( + new Handler(mLooper.getLooper()), + BiometricScheduler.SENSOR_TYPE_FP_OTHER, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + 2 /* recentOperationsLimit */, + () -> USER_ID, + mUserSwitchProvider); + } else { + mScheduler = new UserAwareBiometricScheduler<>(TAG, + new Handler(mLooper.getLooper()), + BiometricScheduler.SENSOR_TYPE_FP_OTHER, + null /* gestureAvailabilityDispatcher */, + mBiometricService, + () -> USER_ID, + mUserSwitchCallback); + } mHalCallback = new AidlResponseHandler(mContext, mScheduler, SENSOR_ID, USER_ID, mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator, mHardwareUnavailableCallback); @@ -153,18 +173,7 @@ public class SensorTest { when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID); when(mClientMonitor.isInterruptable()).thenReturn(false); - final SensorProps sensorProps = new SensorProps(); - sensorProps.commonProps = new CommonProps(); - sensorProps.commonProps.sensorId = 1; - final FingerprintSensorPropertiesInternal internalProp = new - FingerprintSensorPropertiesInternal( - sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength, - sensorProps.commonProps.maxEnrollmentsPerUser, null, - sensorProps.sensorType, false /* resetLockoutRequiresHardwareAuthToken */); - final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext, - null /* handler */, internalProp, mLockoutResetDispatcher, - mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession); - sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher); + final Sensor sensor = getSensor(); mScheduler = sensor.getScheduler(); sensor.mCurrentSession = new AidlSession(0, mock(ISession.class), USER_ID, mHalCallback); @@ -185,4 +194,21 @@ public class SensorTest { verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID)); verify(mAuthSessionCoordinator).resetLockoutFor(eq(USER_ID), anyInt(), anyLong()); } + + private Sensor getSensor() { + final SensorProps sensorProps = new SensorProps(); + sensorProps.commonProps = new CommonProps(); + sensorProps.commonProps.sensorId = 1; + final FingerprintSensorPropertiesInternal internalProp = new + FingerprintSensorPropertiesInternal( + sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength, + sensorProps.commonProps.maxEnrollmentsPerUser, null, + sensorProps.sensorType, false /* resetLockoutRequiresHardwareAuthToken */); + final Sensor sensor = new Sensor(mFingerprintProvider, mContext, + null /* handler */, internalProp, + mBiometricContext, mCurrentSession); + sensor.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcher); + + return sensor; + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java index 89a49615dbe1..cbbc54547bf6 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java @@ -36,12 +36,12 @@ import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.HidlFingerprintSensorConfig; import android.os.Handler; +import android.os.HandlerThread; import android.os.RemoteException; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import android.testing.TestableContext; -import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; @@ -49,17 +49,11 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.AuthenticationStateListeners; -import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricUtils; -import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; -import com.android.server.biometrics.sensors.StartUserClient; -import com.android.server.biometrics.sensors.StopUserClient; -import com.android.server.biometrics.sensors.UserAwareBiometricScheduler; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import com.android.server.biometrics.sensors.fingerprint.aidl.AidlResponseHandler; -import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintEnrollClient; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient; @@ -111,60 +105,18 @@ public class HidlToAidlSensorAdapterTest { private BiometricUtils<Fingerprint> mBiometricUtils; @Mock private AuthenticationStateListeners mAuthenticationStateListeners; + @Mock + private HandlerThread mThread; private final TestLooper mLooper = new TestLooper(); private HidlToAidlSensorAdapter mHidlToAidlSensorAdapter; private final TestableContext mContext = new TestableContext( ApplicationProvider.getApplicationContext()); - private final UserAwareBiometricScheduler.UserSwitchCallback mUserSwitchCallback = - new UserAwareBiometricScheduler.UserSwitchCallback() { - @NonNull - @Override - public StopUserClient<?> getStopUserClient(int userId) { - return new StopUserClient<IBiometricsFingerprint>(mContext, - mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null, USER_ID, - SENSOR_ID, mLogger, mBiometricContext, () -> {}) { - @Override - protected void startHalOperation() { - getCallback().onClientFinished(this, true /* success */); - } - - @Override - public void unableToStart() {} - }; - } - - @NonNull - @Override - public StartUserClient<?, ?> getStartUserClient(int newUserId) { - return new StartUserClient<IBiometricsFingerprint, AidlSession>(mContext, - mHidlToAidlSensorAdapter::getIBiometricsFingerprint, null, - USER_ID, SENSOR_ID, - mLogger, mBiometricContext, - (newUserId1, newUser, halInterfaceVersion) -> - mHidlToAidlSensorAdapter.handleUserChanged(newUserId1)) { - @Override - public void start(@NonNull ClientMonitorCallback callback) { - super.start(callback); - startHalOperation(); - } - - @Override - protected void startHalOperation() { - mUserStartedCallback.onUserStarted(USER_ID, null, 0); - getCallback().onClientFinished(this, true /* success */); - } - - @Override - public void unableToStart() {} - }; - } - };; - @Before public void setUp() throws RemoteException { when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator); + when(mThread.getLooper()).thenReturn(mLooper.getLooper()); doAnswer((answer) -> { mHidlToAidlSensorAdapter.getLazySession().get().getHalSessionCallback() .onEnrollmentProgress(1 /* enrollmentId */, 0 /* remaining */); @@ -175,26 +127,18 @@ public class HidlToAidlSensorAdapterTest { mContext.getOrCreateTestableResources(); final String config = String.format("%d:2:15", SENSOR_ID); - final UserAwareBiometricScheduler scheduler = new UserAwareBiometricScheduler(TAG, - new Handler(mLooper.getLooper()), - BiometricScheduler.SENSOR_TYPE_FP_OTHER, - null /* gestureAvailabilityDispatcher */, - mBiometricService, - () -> USER_ID, - mUserSwitchCallback); final HidlFingerprintSensorConfig fingerprintSensorConfig = new HidlFingerprintSensorConfig(); fingerprintSensorConfig.parse(config, mContext); - mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter(TAG, + mHidlToAidlSensorAdapter = new HidlToAidlSensorAdapter( mFingerprintProvider, mContext, new Handler(mLooper.getLooper()), fingerprintSensorConfig, mLockoutResetDispatcherForSensor, - mGestureAvailabilityDispatcher, mBiometricContext, - false /* resetLockoutRequiresHardwareAuthToken */, + mBiometricContext, false /* resetLockoutRequiresHardwareAuthToken */, mInternalCleanupRunnable, mAuthSessionCoordinator, mDaemon, mAidlResponseHandlerCallback); mHidlToAidlSensorAdapter.init(mGestureAvailabilityDispatcher, mLockoutResetDispatcherForSensor); - mHidlToAidlSensorAdapter.setScheduler(scheduler); + mHidlToAidlSensorAdapter.handleUserChanged(USER_ID); } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 995d1f4d5520..276c8321fb65 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -1982,8 +1982,8 @@ public class VirtualDeviceManagerServiceTest { return new AssociationInfo(associationId, /* userId= */ 0, /* packageName=*/ null, /* tag= */ null, MacAddress.BROADCAST_ADDRESS, /* displayName= */ "", deviceProfile, /* associatedDevice= */ null, /* selfManaged= */ true, - /* notifyOnDeviceNearby= */ false, /* revoked= */false, /* timeApprovedMs= */0, - /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1); + /* notifyOnDeviceNearby= */ false, /* revoked= */ false, /* pending= */ false, + /* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1); } /** Helper class to drop permissions temporarily and restore them at the end of a test. */ diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java index f5d50d173466..6986cab72f56 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java @@ -305,9 +305,9 @@ public abstract class BaseLockSettingsServiceTests { doAnswer(invocation -> { Object[] args = invocation.getArguments(); mStorageManager.unlockCeStorage(/* userId= */ (int) args[0], - /* secret= */ (byte[]) args[2]); + /* secret= */ (byte[]) args[1]); return null; - }).when(sm).unlockCeStorage(anyInt(), anyInt(), any()); + }).when(sm).unlockCeStorage(anyInt(), any()); doAnswer(invocation -> { Object[] args = invocation.getArguments(); diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt index 6a088d99588e..757abde2041e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt +++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt @@ -157,7 +157,7 @@ class AndroidPackageParsingValidationTest { validateTagCount("uses-library", 1000, tag) validateTagCount("activity-alias", 4000, tag) validateTagCount("provider", 8000, tag) - validateTagCount("activity", 40000, tag) + validateTagCount("activity", 30000, tag) } @Test @@ -465,7 +465,8 @@ class AndroidPackageParsingValidationTest { R.styleable.AndroidManifestData_pathAdvancedPattern, 4000 ) - validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 512) + validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 255) + validateTagAttr(tag, "mimeGroup", R.styleable.AndroidManifestData_mimeGroup, 1024) } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java index 42ad73a23f0e..8622488f820e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -158,6 +158,9 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { when(mAudioManager.isAudioFocusExclusive()).thenReturn(false); when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10); + // consistent with focus not exclusive and volume not muted + when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class))) + .thenReturn(true); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); when(mAudioManager.getFocusRampTimeMs(anyInt(), any(AudioAttributes.class))).thenReturn(50); when(mUsageStats.isAlertRateLimited(any())).thenReturn(false); @@ -869,6 +872,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mService.buzzBeepBlinkLocked(r); @@ -886,6 +890,8 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(1); + // all streams at 1 means no muting from audio framework + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(true); mService.buzzBeepBlinkLocked(r); @@ -904,6 +910,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mService.buzzBeepBlinkLocked(r); @@ -924,6 +931,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mService.buzzBeepBlinkLocked(r); @@ -1195,6 +1203,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mService.buzzBeepBlinkLocked(r); verifyDelayedVibrate(mService.getVibratorHelper().createFallbackVibration(false)); @@ -1923,6 +1932,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { NotificationRecord r = getBuzzyBeepyNotification(); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mService.buzzBeepBlinkLocked(r); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index cf8548cfe689..bfd2df2d2b7d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -19,17 +19,21 @@ import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.GROUP_ALERT_ALL; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.GROUP_ALERT_SUMMARY; +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; + import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -178,6 +182,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); when(mAudioManager.getFocusRampTimeMs(anyInt(), any(AudioAttributes.class))).thenReturn(50); + when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class))) + .thenReturn(true); when(mUsageStats.isAlertRateLimited(any())).thenReturn(false); when(mVibrator.hasFrequencyControl()).thenReturn(false); when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(false); @@ -195,7 +201,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // TODO (b/291907312): remove feature flag mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); // Disable feature flags by default. Tests should enable as needed. - mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_EXPIRE_BITMAPS); + mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, + Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED); mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger, mNotificationInstanceIdSequence)); @@ -364,10 +371,20 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } private NotificationRecord getNotificationRecord(int id, - boolean insistent, boolean once, - boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, - boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, - boolean isLeanback, UserHandle userHandle) { + boolean insistent, boolean once, + boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, + boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, + boolean isLeanback, UserHandle userHandle) { + return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration, + defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, userHandle, + mPkg); + } + + private NotificationRecord getNotificationRecord(int id, + boolean insistent, boolean once, + boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, + boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, + boolean isLeanback, UserHandle userHandle, String packageName) { final Builder builder = new Builder(getContext()) .setContentTitle("foo") @@ -427,8 +444,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) .thenReturn(isLeanback); - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid, - mPid, n, userHandle, null, System.currentTimeMillis()); + StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, id, mTag, + mUid, mPid, n, userHandle, null, System.currentTimeMillis()); NotificationRecord r = new NotificationRecord(context, sbn, mChannel); mService.addNotification(r); return r; @@ -915,6 +932,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); + when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class))) + .thenReturn(false); mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); @@ -932,6 +951,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(1); + // all streams at 1 means no muting from audio framework + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(true); mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); @@ -950,6 +971,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); @@ -971,6 +993,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); @@ -1243,6 +1266,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // the phone is quiet when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); verifyDelayedVibrate(mAttentionHelper.getVibratorHelper().createFallbackVibration(false)); @@ -1973,6 +1997,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { NotificationRecord r = getBuzzyBeepyNotification(); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); + when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); @@ -1990,7 +2015,6 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { public void testBeepVolume_politeNotif() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); - flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1"); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); initAttentionHelper(flagResolver); @@ -2015,13 +2039,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { assertNotEquals(-1, r.getLastAudiblyAlertedMs()); } - // TODO b/270456865: Only one of the two strategies will be released. - // The other one need to be removed @Test - public void testBeepVolume_politeNotif_Strategy2() throws Exception { + public void testBeepVolume_politeNotif_GlobalStrategy() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); - flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule2"); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); initAttentionHelper(flagResolver); @@ -2032,14 +2054,58 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); Mockito.reset(mRingtonePlayer); - // update should beep at 0% volume - r.isUpdate = true; - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + // Use different package for next notifications + NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */, + true /* noisy */, false /* buzzy*/, false /* lights */, true, true, + false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); + + // update should beep at 50% volume + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + verifyBeepVolume(0.5f); + + // Use different package for next notifications + NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, + true /* noisy */, false /* buzzy*/, false /* lights */, true, true, + false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg"); + + // 2nd update should beep at 0% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); verifyBeepVolume(0.0f); + verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + assertNotEquals(-1, r.getLastAudiblyAlertedMs()); + } + + @Test + public void testBeepVolume_politeNotif_GlobalStrategy_ChannelHasUserSound() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + initAttentionHelper(flagResolver); + + NotificationRecord r = getBeepyNotification(); + + // set up internal state + mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + Mockito.reset(mRingtonePlayer); + + // Use package with user-set sounds for next notifications + mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT); + mChannel.lockFields(NotificationChannel.USER_LOCKED_SOUND); + NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */, + true /* noisy */, false /* buzzy*/, false /* lights */, true, true, + false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); + + // update should beep at 100% volume + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + verifyBeepVolume(1.0f); + // 2nd update should beep at 50% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); verifyBeepVolume(0.5f); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); @@ -2047,39 +2113,101 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test - public void testVibrationIntensity_politeNotif() throws Exception { + public void testBeepVolume_politeNotif_applyPerApp() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); - flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1"); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); initAttentionHelper(flagResolver); - NotificationRecord r = getBuzzyBeepyNotification(); + NotificationRecord r = getBeepyNotification(); // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + Mockito.reset(mRingtonePlayer); - VibratorHelper vibratorHelper = mAttentionHelper.getVibratorHelper(); - Mockito.reset(vibratorHelper); + // Use different channel for next notifications + mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT); - // update should buzz at 50% intensity - r.isUpdate = true; - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + // update should beep at 50% volume + NotificationRecord r2 = getBeepyNotification(); + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + verifyBeepVolume(0.5f); - verify(vibratorHelper, times(1)).scale(any(), eq(0.5f)); - Mockito.reset(vibratorHelper); + // 2nd update should beep at 0% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + verifyBeepVolume(0.0f); - // 2nd update should buzz at 0% intensity + // Use different package for next notifications + NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, + true /* noisy */, false /* buzzy*/, false /* lights */, true, true, + false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); + + // Update from new package should beep at 100% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyBeepVolume(1.0f); + + verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); + assertNotEquals(-1, r.getLastAudiblyAlertedMs()); + } + + @Test + public void testBeepVolume_politeNotif_applyPerApp_ChannelHasUserSound() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + NotificationRecord r = getBeepyNotification(); + + // set up internal state mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verify(vibratorHelper, times(1)).scale(any(), eq(0.0f)); + Mockito.reset(mRingtonePlayer); + + // Use different channel for next notifications + mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT); + mChannel.lockFields(NotificationChannel.USER_LOCKED_SOUND); + + // update should beep at 100% volume + NotificationRecord r2 = getBeepyNotification(); + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + verifyBeepVolume(1.0f); + + // 2nd update should beep at 50% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + verifyBeepVolume(0.5f); + + // Use different package for next notifications + mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT); + NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, + true /* noisy */, false /* buzzy*/, false /* lights */, true, true, + false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); + + // Update from new package should beep at 100% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyBeepVolume(1.0f); + + verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); + assertNotEquals(-1, r.getLastAudiblyAlertedMs()); } @Test - public void testVibrationIntensity_politeNotif_Strategy2() throws Exception { + public void testVibrationIntensity_politeNotif() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); - flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule2"); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); initAttentionHelper(flagResolver); @@ -2092,21 +2220,22 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { VibratorHelper vibratorHelper = mAttentionHelper.getVibratorHelper(); Mockito.reset(vibratorHelper); - // update should buzz at 0% intensity + // update should buzz at 50% intensity r.isUpdate = true; mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verify(vibratorHelper, times(1)).scale(any(), eq(0.0f)); + verify(vibratorHelper, times(1)).scale(any(), eq(0.5f)); Mockito.reset(vibratorHelper); - // 2nd update should buzz at 50% intensity + // 2nd update should buzz at 0% intensity mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verify(vibratorHelper, times(1)).scale(any(), eq(0.5f)); + verify(vibratorHelper, times(1)).scale(any(), eq(0.0f)); } @Test public void testBuzzOnlyOnScreenUnlock_politeNotif() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_VIBRATE_WHILE_UNLOCKED); TestableFlagResolver flagResolver = new TestableFlagResolver(); // When NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED setting is enabled @@ -2161,7 +2290,6 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { public void testBeepVolume_politeNotif_workProfile() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); - flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1"); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); @@ -2202,7 +2330,6 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { public void testBeepVolume_politeNotif_workProfile_disabled() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); - flagResolver.setFlagOverride(NotificationFlags.NOTIF_COOLDOWN_RULE, "rule1"); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java index 5892793fdb72..c10c3c28e9dd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java @@ -170,6 +170,10 @@ public class NotificationHistoryManagerTest extends UiServiceTestCase { Settings.Secure.putIntForUser(getContext().getContentResolver(), Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, USER_SYSTEM); mHistoryManager.mSettingsObserver.update(null, USER_SYSTEM); + // fake cloned settings to profile + Settings.Secure.putIntForUser(getContext().getContentResolver(), + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, mProfileId); + mHistoryManager.mSettingsObserver.update(null, mProfileId); // unlock user, verify that history is disabled for self and profile mHistoryManager.onUserUnlocked(USER_SYSTEM); @@ -181,6 +185,37 @@ public class NotificationHistoryManagerTest extends UiServiceTestCase { } @Test + public void testAddProfile_historyEnabledInPrimary() { + // create a history + mHistoryManager.onUserUnlocked(MIN_SECONDARY_USER_ID); + assertThat(mHistoryManager.doesHistoryExistForUser(MIN_SECONDARY_USER_ID)).isTrue(); + + // fake Settings#CLONE_TO_MANAGED_PROFILE + int newProfileId = 99; + Settings.Secure.putIntForUser(getContext().getContentResolver(), + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1, newProfileId); + mUsers = new ArrayList<>(); + UserInfo userFullSecondary = new UserInfo(); + userFullSecondary.id = MIN_SECONDARY_USER_ID; + mUsers.add(userFullSecondary); + UserInfo userProfile = new UserInfo(); + userProfile.id = newProfileId; + mUsers.add(userProfile); + when(mUserManager.getUsers()).thenReturn(mUsers); + + mProfiles = new int[] {MIN_SECONDARY_USER_ID, userProfile.id}; + when(mUserManager.getProfileIds(MIN_SECONDARY_USER_ID, true)).thenReturn(mProfiles); + when(mUserManager.getProfileIds(userProfile.id, true)) + .thenReturn(new int[] {userProfile.id}); + when(mUserManager.getProfileParent(userProfile.id)).thenReturn(userFullSecondary); + + // add profile + mHistoryManager.onUserAdded(newProfileId); + mHistoryManager.onUserUnlocked(newProfileId); + assertThat(mHistoryManager.doesHistoryExistForUser(newProfileId)).isTrue(); + } + + @Test public void testOnUserUnlocked_historyDisabledThenEnabled() { // create a history mHistoryManager.onUserUnlocked(USER_SYSTEM); 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 f1edd9a59b99..c1f35ccb69e0 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -958,22 +958,24 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestNotificationChannel.setAllowBubbles(channelEnabled); } - private void setUpPrefsForHistory(int uid, boolean globalEnabled) throws Exception { + private void setUpPrefsForHistory(@UserIdInt int userId, boolean globalEnabled) + throws Exception { initNMS(SystemService.PHASE_ACTIVITY_MANAGER_READY); // Sets NOTIFICATION_HISTORY_ENABLED setting for calling process uid Settings.Secure.putIntForUser(mContext.getContentResolver(), - Settings.Secure.NOTIFICATION_HISTORY_ENABLED, globalEnabled ? 1 : 0, uid); + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, globalEnabled ? 1 : 0, userId); // Sets NOTIFICATION_HISTORY_ENABLED setting for uid 0 Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.NOTIFICATION_HISTORY_ENABLED, globalEnabled ? 1 : 0); + setUsers(new int[] {0, userId}); // Forces an update by calling observe on mSettingsObserver, which picks up the settings // changes above. mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START, mMainLooper); assertEquals(globalEnabled, Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0 /* =def */, uid) != 0); + Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0 /* =def */, userId) != 0); } private StatusBarNotification generateSbn(String pkg, int uid, long postTime, int userId) { @@ -10443,7 +10445,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testHandleOnPackageRemoved_ClearsHistory() throws Exception { // Enables Notification History setting - setUpPrefsForHistory(mUid, true /* =enabled */); + setUpPrefsForHistory(mUserId, true /* =enabled */); // Posts a notification to the mTestNotificationChannel. final NotificationRecord notif = generateNotificationRecord( 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 999e33c24322..3d8ec2ec9277 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java @@ -18,19 +18,31 @@ package com.android.server.notification; import static com.google.common.truth.Truth.assertThat; +import android.app.Flags; import android.os.Parcel; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenDeviceEffects; import androidx.test.runner.AndroidJUnit4; import com.android.server.UiServiceTestCase; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class ZenDeviceEffectsTest extends UiServiceTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public final void setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + } + @Test public void builder() { ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() @@ -40,6 +52,7 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .setShouldUseNightMode(false) .setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true) + .setUserModifiedFields(8) .build(); assertThat(deviceEffects.shouldDimWallpaper()).isTrue(); @@ -52,6 +65,7 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse(); assertThat(deviceEffects.shouldUseNightMode()).isFalse(); assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue(); + assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(8); } @Test @@ -83,6 +97,7 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMinimizeRadioUsage(true) .setShouldUseNightMode(true) .setShouldSuppressAmbientDisplay(true) + .setUserModifiedFields(6) .build(); Parcel parcel = Parcel.obtain(); @@ -101,6 +116,7 @@ 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 3185c50c27ef..177d64555899 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -19,12 +19,15 @@ package com.android.server.notification; import static android.app.AutomaticZenRule.TYPE_BEDTIME; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.assertTrue; +import android.app.AutomaticZenRule; import android.app.Flags; import android.app.NotificationManager.Policy; import android.content.ComponentName; @@ -46,6 +49,7 @@ import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.UiServiceTestCase; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -83,6 +87,11 @@ public class ZenModeConfigTest extends UiServiceTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before + public final void setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + } + @Test public void testPriorityOnlyMutingAllNotifications() { ZenModeConfig config = getMutedRingerConfig(); @@ -275,6 +284,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders()); assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders()); assertEquals(expected.getAllowedChannels(), actual.getAllowedChannels()); + assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields()); } @Test @@ -327,6 +337,53 @@ public class ZenModeConfigTest extends UiServiceTestCase { } @Test + public void testCanBeUpdatedByApp_nullPolicyAndDeviceEffects() throws Exception { + 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); + assertThat(rule.canBeUpdatedByApp()).isTrue(); + + policy = policyBuilder.setUserModifiedFields(1).build(); + assertThat(policy.getUserModifiedFields()).isEqualTo(1); + rule.zenPolicy = policy; + 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); + assertThat(rule.canBeUpdatedByApp()).isTrue(); + + deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build(); + assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1); + rule.zenDeviceEffects = deviceEffects; + assertThat(rule.canBeUpdatedByApp()).isFalse(); + } + + @Test public void testWriteToParcel() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); @@ -347,6 +404,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; + rule.userModifiedFields = 16; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; @@ -371,6 +429,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.allowManualInvocation, parceled.allowManualInvocation); assertEquals(rule.iconResName, parceled.iconResName); assertEquals(rule.type, parceled.type); + assertEquals(rule.userModifiedFields, parceled.userModifiedFields); assertEquals(rule.triggerDescription, parceled.triggerDescription); assertEquals(rule.zenPolicy, parceled.zenPolicy); assertEquals(rule, parceled); @@ -448,6 +507,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; + rule.userModifiedFields = 4; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; @@ -476,6 +536,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation); assertEquals(rule.type, fromXml.type); + assertEquals(rule.userModifiedFields, fromXml.userModifiedFields); assertEquals(rule.triggerDescription, fromXml.triggerDescription); assertEquals(rule.iconResName, fromXml.iconResName); } @@ -550,6 +611,22 @@ public class ZenModeConfigTest extends UiServiceTestCase { } @Test + public void testRuleXml_userModifiedField() throws Exception { + ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); + rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; + assertThat(rule.userModifiedFields).isEqualTo(1); + assertThat(rule.canBeUpdatedByApp()).isFalse(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeRuleXml(rule, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig.ZenRule fromXml = readRuleXml(bais); + + assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields); + assertThat(fromXml.canBeUpdatedByApp()).isFalse(); + } + + @Test public void testZenPolicyXml_classic() throws Exception { ZenPolicy policy = new ZenPolicy.Builder() .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) @@ -615,6 +692,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) .hideAllVisualEffects() .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true) + .setUserModifiedFields(4) .build(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -649,6 +727,7 @@ 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 93cd44eb7966..7e92e427b9a4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -76,7 +76,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { ? Set.of() : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION, RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL, - RuleDiff.FIELD_ZEN_DEVICE_EFFECTS); + RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, RuleDiff.FIELD_USER_MODIFIED_FIELDS); // allowPriorityChannels is flagged by android.app.modes_api public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS = @@ -304,6 +304,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { rule.zenDeviceEffects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .build(); + rule.userModifiedFields = AutomaticZenRule.FIELD_NAME; } return rule; } 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 f84d8e95e426..0224ff35219b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -21,6 +21,9 @@ import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED; +import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; +import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS; @@ -182,8 +185,9 @@ import java.util.concurrent.TimeUnit; @TestableLooper.RunWithLooper public class ZenModeHelperTest extends UiServiceTestCase { - private static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE"; - private static final String SCHEDULE_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE"; + private static final String EVENTS_DEFAULT_RULE_ID = ZenModeConfig.EVENTS_DEFAULT_RULE_ID; + private static final String SCHEDULE_DEFAULT_RULE_ID = + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID; private static final String CUSTOM_PKG_NAME = "not.android"; private static final String CUSTOM_APP_LABEL = "This is not Android"; private static final int CUSTOM_PKG_UID = 1; @@ -2197,7 +2201,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void addAutomaticZenRule_fromUser_respectsHiddenEffects() { + public void addAutomaticZenRule_fromUser_respectsHiddenEffects() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); ZenDeviceEffects zde = new ZenDeviceEffects.Builder() @@ -2222,7 +2226,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); + + // 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); } @Test @@ -2298,8 +2308,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder() - .setShouldUseNightMode(true) // Good - .setShouldMaximizeDoze(true) // Also good + .setShouldUseNightMode(true) + .setShouldMaximizeDoze(true) + // Just to emphasize that unset values default to false; + // even with this line removed, tap to wake would be set to false. + .setShouldDisableTapToWake(false) .build(); mZenModeHelper.updateAutomaticZenRule(ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) @@ -2308,7 +2321,75 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_USER, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser); + + // 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); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() { + ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); + sleepingRule.enabled = false; + sleepingRule.userModifiedFields = 0; + sleepingRule.name = "ZZZZZZZ..."; + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule); + + AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) + .setType(TYPE_BEDTIME) + .build(); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + "reason", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(bedtimeRuleId); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void addAutomaticZenRule_withTypeBedtime_keepsEnabledSleeping() { + ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); + sleepingRule.enabled = true; + sleepingRule.userModifiedFields = 0; + sleepingRule.name = "ZZZZZZZ..."; + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule); + + AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) + .setType(TYPE_BEDTIME) + .build(); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + "reason", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, bedtimeRuleId); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void addAutomaticZenRule_withTypeBedtime_keepsCustomizedSleeping() { + ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); + sleepingRule.enabled = false; + sleepingRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; + sleepingRule.name = "ZZZZZZZ..."; + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule); + + AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) + .setType(TYPE_BEDTIME) + .build(); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + "reason", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( + ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, bedtimeRuleId); } @Test @@ -3321,6 +3402,7 @@ 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; @@ -3335,6 +3417,7 @@ 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()); @@ -3376,10 +3459,480 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ALLOW_MANUAL, rule.allowManualInvocation); assertEquals(OWNER.getPackageName(), rule.getPkg()); assertEquals(ICON_RES_NAME, rule.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); } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_updatesNameUnlessUserModified() { + // Add a starting rule with the name OriginalName. + AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // Checks the name can be changed by the app because the user has not modified it. + AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) + .setName("NewName") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", + 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. + azrUpdate = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .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. + azrUpdate = new AutomaticZenRule.Builder(rule) + .setName("NewAppName") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + assertThat(rule.getName()).isEqualTo("NewAppName"); + + // The user modifies the name. + azrUpdate = new AutomaticZenRule.Builder(rule) + .setName("UserProvidedName") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", + 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) + .setName("NewAppName") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + assertThat(rule.getName()).isEqualTo("UserProvidedName"); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_updatesBitmaskAndValueForUserOrigin() { + // Adds a starting rule with empty zen policies and device effects + AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) + .setZenPolicy(new ZenPolicy.Builder().build()) + .setDeviceEffects(new ZenDeviceEffects.Builder().build()) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // Modifies the zen policy and device effects + ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) + .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .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(); + + // Update the rule with the AZR from origin user. + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // 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().getAllowedChannels()) + .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + 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(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_updatesValuesForSystemUiOrigin() { + // Adds a starting rule with empty zen policies and device effects + AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_ALL) + .setZenPolicy(new ZenPolicy.Builder() + .allowReminders(false) + .build()) + .setDeviceEffects(new ZenDeviceEffects.Builder() + .setShouldDisplayGrayscale(false) + .build()) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // Modifies the zen policy and device effects + 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(); + + // Update the rule with the AZR from origin systemUI. + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + "reason", Process.SYSTEM_UID); + 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(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_updatesValuesIfRuleNotUserModified() { + // Adds a starting rule with empty zen policies and device effects + AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_ALL) + .setZenPolicy(new ZenPolicy.Builder() + .allowReminders(false) + .build()) + .setDeviceEffects(new ZenDeviceEffects.Builder() + .setShouldDisplayGrayscale(false) + .build()) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + 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) + .build(); + ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() + .setShouldDisplayGrayscale(true) + .build(); + 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. + // The bitmask is not modified. + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_UNKNOWN, "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()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo( + rule.getDeviceEffects().getUserModifiedFields()); + assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + + // 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(); + + // Zen rule update coming from unknown origin. This cannot fully update the rule, because + // the rule is already considered user modified. + mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_UNKNOWN, + "reason", Process.SYSTEM_UID); + ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); + + // UPDATE_ORIGIN_UNKNOWN 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(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_updatesValuesIfRuleNew() { + // Adds a starting rule with empty zen policies and device effects + AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setZenPolicy(new ZenPolicy.Builder() + .allowReminders(true) + .build()) + .setDeviceEffects(new ZenDeviceEffects.Builder() + .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); + 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(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_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()) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) + // Sets Device Effects to null + .setDeviceEffects(null) + .build(); + + // Zen rule update coming from unknown origin, but since the rule isn't already + // user modified, it can be updated. + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // When AZR's ZenDeviceEffects is null, the updated rule's device effects will be null. + assertThat(rule.getDeviceEffects()).isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_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()) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + 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 + // user modified, it can be updated. + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null. + assertThat(rule.getZenPolicy()).isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_nullToNonNullPolicyUpdate() { + when(mContext.checkCallingPermission(anyString())) + .thenReturn(PackageManager.PERMISSION_GRANTED); + // Adds a starting rule with empty zen policies and device effects + AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) + .setZenPolicy(null) + // .setDeviceEffects(new ZenDeviceEffects.Builder().build()) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + 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() + .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) // Differs from the default + .allowReminders(true) // Differs from the default + .allowEvents(true) // Differs from the default + .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT) + .allowMessages(PEOPLE_TYPE_STARRED) + .allowCalls(PEOPLE_TYPE_STARRED) + .allowRepeatCallers(true) + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) // Differs from the default + .showFullScreenIntent(true) // Differs from the default + .showLights(true) // Differs from the default + .showPeeking(true) // Differs from the default + .showStatusBarIcons(true) + .showBadges(true) + .showInAmbientDisplay(true) // Differs from the default + .showInNotificationList(true) + .build(); + AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) + .setZenPolicy(policy) + .build(); + + // Applies the update to the rule. + // Default config defined in getDefaultConfigParser() is used as the original rule. + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // New ZenPolicy differs from the default config + assertThat(rule.getZenPolicy()).isNotNull(); + assertThat(rule.getZenPolicy().getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); + assertThat(rule.canUpdate()).isFalse(); + assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo( + ZenPolicy.FIELD_ALLOW_CHANNELS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM + | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT + | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS + | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK + | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT + ); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void automaticZenRuleToZenRule_nullToNonNullDeviceEffectsUpdate() { + // Adds a starting rule with empty zen policies and device effects + AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) + .setDeviceEffects(null) + .build(); + // Adds the rule using the app, to avoid having any user modified bits set. + 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) + .build(); + AutomaticZenRule azr = new AutomaticZenRule.Builder(rule) + .setDeviceEffects(deviceEffects) + .build(); + + // Applies the update to the rule. + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(ruleId); + + // 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( + ZenDeviceEffects.FIELD_GRAYSCALE); + } + + @Test public void testUpdateAutomaticRule_disabled_triggersBroadcast() throws Exception { setupZenConfig(); 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 2f4f891ce982..21c96d6adc7e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -34,6 +34,7 @@ import com.android.server.UiServiceTestCase; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,6 +50,11 @@ public class ZenPolicyTest extends UiServiceTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before + public final void setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + } + @Test public void testZenPolicyApplyAllowedToDisallowed() { ZenPolicy.Builder builder = new ZenPolicy.Builder(); @@ -640,6 +646,54 @@ 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) + .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .setUserModifiedFields(20).build(); + + ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build(); + + assertThat(newPolicy.getPriorityCategoryAlarms()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(newPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(newPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW); + + assertThat(newPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); + + assertThat(newPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20); + } + + @Test public void testTooLongLists_fromParcel() { ArrayList<Integer> longList = new ArrayList<Integer>(50); for (int i = 0; i < 50; i++) { diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java new file mode 100644 index 000000000000..c3da903c9ef1 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java @@ -0,0 +1,354 @@ +/* + * 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.policy; + +import static android.os.PowerManager.WAKE_REASON_CAMERA_LAUNCH; +import static android.os.PowerManager.WAKE_REASON_LID; +import static android.os.PowerManager.WAKE_REASON_GESTURE; +import static android.os.PowerManager.WAKE_REASON_POWER_BUTTON; +import static android.os.PowerManager.WAKE_REASON_WAKE_KEY; +import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION; +import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; +import static android.view.InputDevice.SOURCE_TOUCHSCREEN; +import static android.view.KeyEvent.KEYCODE_HOME; +import static android.view.KeyEvent.KEYCODE_POWER; +import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY; + +import static com.android.internal.R.bool.config_allowTheaterModeWakeFromKey; +import static com.android.internal.R.bool.config_allowTheaterModeWakeFromPowerKey; +import static com.android.internal.R.bool.config_allowTheaterModeWakeFromMotion; +import static com.android.internal.R.bool.config_allowTheaterModeWakeFromCameraLens; +import static com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch; +import static com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture; +import static com.android.server.policy.Flags.FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Resources; +import android.os.PowerManager; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.test.InstrumentationRegistry; + +import com.android.internal.os.Clock; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; +import com.android.server.LocalServices; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.function.BooleanSupplier; +/** + * Test class for {@link WindowWakeUpPolicy}. + * + * <p>Build/Install/Run: atest WmTests:WindowWakeUpPolicyTests + */ +public final class WindowWakeUpPolicyTests { + @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock PowerManager mPowerManager; + @Mock Clock mClock; + @Mock WindowWakeUpPolicyInternal.InputWakeUpDelegate mInputWakeUpDelegate; + + private Context mContextSpy; + private Resources mResourcesSpy; + + private WindowWakeUpPolicy mPolicy; + + @Before + public void setUp() { + mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); + mResourcesSpy = spy(mContextSpy.getResources()); + when(mContextSpy.getResources()).thenReturn(mResourcesSpy); + when(mContextSpy.getSystemService(PowerManager.class)).thenReturn(mPowerManager); + LocalServices.removeServiceForTest(WindowWakeUpPolicyInternal.class); + } + + @Test + public void testSupportsInputWakeDelegatse_publishesLocalService() { + mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); + + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + + assertThat(LocalServices.getService(WindowWakeUpPolicyInternal.class)).isNotNull(); + } + + @Test + public void testDoesNotSupportInputWakeDelegatse_doesNotPublishLocalService() { + mSetFlagsRule.disableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); + + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + + assertThat(LocalServices.getService(WindowWakeUpPolicyInternal.class)).isNull(); + } + + @Test + public void testMotionWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() { + setTheaterModeEnabled(false); + mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + LocalServices.getService(WindowWakeUpPolicyInternal.class) + .setInputWakeUpDelegate(mInputWakeUpDelegate); + + setDelegatedMotionWakeUpResult(true); + + // Verify the policy wake up call succeeds because of the call on the delegate, and not + // because of a PowerManager wake up. + assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isTrue(); + verify(mInputWakeUpDelegate).wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true); + verifyNoPowerManagerWakeUp(); + + setDelegatedMotionWakeUpResult(false); + + // Verify the policy wake up call succeeds because of the PowerManager wake up, since the + // delegate would not handle the wake up request. + assertThat(mPolicy.wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false)).isTrue(); + verify(mInputWakeUpDelegate).wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false); + verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION"); + } + + @Test + public void testKeyWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() { + setTheaterModeEnabled(false); + mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + LocalServices.getService(WindowWakeUpPolicyInternal.class) + .setInputWakeUpDelegate(mInputWakeUpDelegate); + + setDelegatedKeyWakeUpResult(true); + + // Verify the policy wake up call succeeds because of the call on the delegate, and not + // because of a PowerManager wake up. + assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isTrue(); + verify(mInputWakeUpDelegate).wakeUpFromKey(200, KEYCODE_POWER, true); + verifyNoPowerManagerWakeUp(); + + setDelegatedKeyWakeUpResult(false); + + // Verify the policy wake up call succeeds because of the PowerManager wake up, since the + // delegate would not handle the wake up request. + assertThat(mPolicy.wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false)).isTrue(); + verify(mInputWakeUpDelegate).wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false); + verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_KEY, "android.policy:KEY"); + } + + @Test + public void testDelegatedKeyWakeIsSubjectToPolicyChecks() { + mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); + setDelegatedKeyWakeUpResult(true); + setTheaterModeEnabled(true); + setBooleanRes(config_allowTheaterModeWakeFromKey, false); + setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + LocalServices.getService(WindowWakeUpPolicyInternal.class) + .setInputWakeUpDelegate(mInputWakeUpDelegate); + + // Check that the wake up does not happen because the theater mode policy check fails. + assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isFalse(); + verify(mInputWakeUpDelegate, never()).wakeUpFromKey(anyLong(), anyInt(), anyBoolean()); + } + + @Test + public void testDelegatedMotionWakeIsSubjectToPolicyChecks() { + mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE); + setDelegatedMotionWakeUpResult(true); + setTheaterModeEnabled(true); + setBooleanRes(config_allowTheaterModeWakeFromMotion, false); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + LocalServices.getService(WindowWakeUpPolicyInternal.class) + .setInputWakeUpDelegate(mInputWakeUpDelegate); + + // Check that the wake up does not happen because the theater mode policy check fails. + assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isFalse(); + verify(mInputWakeUpDelegate, never()).wakeUpFromMotion(anyLong(), anyInt(), anyBoolean()); + } + + @Test + public void testWakeUpFromMotion() { + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromMotion(mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true), + config_allowTheaterModeWakeFromMotion, + WAKE_REASON_WAKE_MOTION, + "android.policy:MOTION"); + } + + @Test + public void testWakeUpFromKey_nonPowerKey() { + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_HOME, true), + config_allowTheaterModeWakeFromKey, + WAKE_REASON_WAKE_KEY, + "android.policy:KEY"); + } + + @Test + public void testWakeUpFromKey_powerKey() { + // Disable the resource affecting all wake keys because it affects power key as well. + // That way, power key wake during theater mode will solely be controlled by + // `config_allowTheaterModeWakeFromPowerKey` in the checks. + setBooleanRes(config_allowTheaterModeWakeFromKey, false); + + // Test with power key + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, true), + config_allowTheaterModeWakeFromPowerKey, + WAKE_REASON_POWER_BUTTON, + "android.policy:POWER"); + + // Test that power key wake ups happen during theater mode as long as wake-keys are allowed + // even if the power-key specific theater mode config is disabled. + setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false); + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, false), + config_allowTheaterModeWakeFromKey, + WAKE_REASON_POWER_BUTTON, + "android.policy:POWER"); + } + + @Test + public void testWakeUpFromLid() { + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromLid(), + config_allowTheaterModeWakeFromLidSwitch, + WAKE_REASON_LID, + "android.policy:LID"); + } + + @Test + public void testWakeUpFromWakeGesture() { + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromWakeGesture(), + config_allowTheaterModeWakeFromGesture, + WAKE_REASON_GESTURE, + "android.policy:GESTURE"); + } + + @Test + public void testwakeUpFromCameraCover() { + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromCameraCover(mClock.uptimeMillis()), + config_allowTheaterModeWakeFromCameraLens, + WAKE_REASON_CAMERA_LAUNCH, + "android.policy:CAMERA_COVER"); + } + + @Test + public void testWakeUpFromPowerKeyCameraGesture() { + // Disable the resource affecting all wake keys because it affects power key as well. + // That way, power key wake during theater mode will solely be controlled by + // `config_allowTheaterModeWakeFromPowerKey` in the checks. + setBooleanRes(config_allowTheaterModeWakeFromKey, false); + + runPowerManagerUpChecks( + () -> mPolicy.wakeUpFromPowerKeyCameraGesture(), + config_allowTheaterModeWakeFromPowerKey, + WAKE_REASON_CAMERA_LAUNCH, + "android.policy:CAMERA_GESTURE_PREVENT_LOCK"); + } + + private void runPowerManagerUpChecks( + BooleanSupplier wakeUpCall, + int theatherModeWakeResId, + int expectedWakeReason, + String expectedWakeDetails) { + // Test under theater mode enabled. + setTheaterModeEnabled(true); + + Mockito.reset(mPowerManager); + setBooleanRes(theatherModeWakeResId, true); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + setUptimeMillis(200); + assertWithMessage("Wake should happen in theater mode when config allows it.") + .that(wakeUpCall.getAsBoolean()).isTrue(); + verify(mPowerManager).wakeUp(200L, expectedWakeReason, expectedWakeDetails); + + Mockito.reset(mPowerManager); + setBooleanRes(theatherModeWakeResId, false); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + setUptimeMillis(250); + assertWithMessage("Wake should not happen in theater mode when config disallows it.") + .that(wakeUpCall.getAsBoolean()).isFalse(); + verifyNoPowerManagerWakeUp(); + + // Cases when theater mode is disabled. + setTheaterModeEnabled(false); + + Mockito.reset(mPowerManager); + setBooleanRes(theatherModeWakeResId, true); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + setUptimeMillis(300); + assertWithMessage("Wake should happen when not in theater mode.") + .that(wakeUpCall.getAsBoolean()).isTrue(); + verify(mPowerManager).wakeUp(300L, expectedWakeReason, expectedWakeDetails); + + Mockito.reset(mPowerManager); + setBooleanRes(theatherModeWakeResId, false); + mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock); + setUptimeMillis(350); + assertWithMessage("Wake should happen when not in theater mode.") + .that(wakeUpCall.getAsBoolean()).isTrue(); + verify(mPowerManager).wakeUp(350L, expectedWakeReason, expectedWakeDetails); + } + + private void verifyNoPowerManagerWakeUp() { + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + } + + private void setBooleanRes(int resId, boolean val) { + when(mResourcesSpy.getBoolean(resId)).thenReturn(val); + } + + private void setUptimeMillis(long uptimeMillis) { + when(mClock.uptimeMillis()).thenReturn(uptimeMillis); + } + + private void setTheaterModeEnabled(boolean enabled) { + Settings.Global.putInt( + mContextSpy.getContentResolver(), Settings.Global.THEATER_MODE_ON, enabled ? 1 : 0); + } + + private void setDelegatedMotionWakeUpResult(boolean result) { + when(mInputWakeUpDelegate.wakeUpFromMotion(anyLong(), anyInt(), anyBoolean())) + .thenReturn(result); + } + + private void setDelegatedKeyWakeUpResult(boolean result) { + when(mInputWakeUpDelegate.wakeUpFromKey(anyLong(), anyInt(), anyBoolean())) + .thenReturn(result); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java new file mode 100644 index 000000000000..71dbc57e5065 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java @@ -0,0 +1,104 @@ +/* + * 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.wm; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.SensitiveContentPackages.PackageInfo; + +import org.junit.After; +import org.junit.Test; + +import java.util.Collections; +import java.util.Set; + +/** + * Build/Install/Run: + * atest WmTests:SensitiveContentPackagesTest + */ +@SmallTest +@Presubmit +public class SensitiveContentPackagesTest { + private static final String APP_PKG_1 = "com.android.server.wm.one"; + private static final String APP_PKG_2 = "com.android.server.wm.two"; + private static final String APP_PKG_3 = "com.android.server.wm.three"; + + private static final int APP_UID_1 = 5; + private static final int APP_UID_2 = 6; + private static final int APP_UID_3 = 7; + + + private final SensitiveContentPackages mSensitiveContentPackages = + new SensitiveContentPackages(); + + @After + public void tearDown() { + mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + + @Test + public void setShouldBlockScreenCaptureForApp() { + Set<PackageInfo> blockedApps = + 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.setShouldBlockScreenCaptureForApp(blockedApps); + + 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 setShouldBlockScreenCaptureForApp_empty() { + Set<PackageInfo> blockedApps = + 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.setShouldBlockScreenCaptureForApp(blockedApps); + mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + + 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)); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index 810cbe8f8080..6c5f9752b6fc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -134,6 +135,33 @@ public class SyncEngineTests extends WindowTestsBase { assertFalse(r.isSyncFinished(r.getSyncGroup())); r.finishRelaunching(); assertTrue(r.isSyncFinished(r.getSyncGroup())); + assertEquals(SYNC_STATE_READY, r.mSyncState); + + // If the container has finished the sync, isSyncFinished should not change the sync state. + final BLASTSyncEngine.SyncGroup syncGroup = r.getSyncGroup(); + r.finishSync(mTransaction, syncGroup, false /* cancel */); + assertEquals(SYNC_STATE_NONE, r.mSyncState); + assertTrue(r.isSyncFinished(syncGroup)); + assertEquals(SYNC_STATE_NONE, r.mSyncState); + } + + @Test + public void testFinishSyncByStartingWindow() { + final ActivityRecord taskRoot = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task task = taskRoot.getTask(); + final ActivityRecord translucentTop = new ActivityBuilder(mAtm).setTask(task) + .setActivityTheme(android.R.style.Theme_Translucent).build(); + createWindow(null, TYPE_BASE_APPLICATION, taskRoot, "win"); + final WindowState startingWindow = createWindow(null, TYPE_APPLICATION_STARTING, + translucentTop, "starting"); + startingWindow.mStartingData = new SnapshotStartingData(mWm, null, 0); + task.mSharedStartingData = startingWindow.mStartingData; + task.prepareSync(); + + final BLASTSyncEngine.SyncGroup group = mock(BLASTSyncEngine.SyncGroup.class); + assertFalse(task.isSyncFinished(group)); + startingWindow.onSyncFinishedDrawing(); + assertTrue(task.isSyncFinished(group)); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 45e1e9579f3b..b36080023ef2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -43,6 +43,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.server.policy.WindowManagerPolicy.USER_ROTATION_FREE; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; +import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_PARENT_TASK; import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT; import static com.google.common.truth.Truth.assertThat; @@ -1620,6 +1621,29 @@ public class TaskTests extends WindowTestsBase { } @Test + public void testBoostDimmingTaskFragmentOnTask() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final Task task = createTask(mDisplayContent); + final TaskFragment primary = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment secondary = createTaskFragmentWithEmbeddedActivity(task, organizer); + final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + + primary.mVisibleRequested = true; + secondary.mVisibleRequested = true; + primary.setAdjacentTaskFragment(secondary); + secondary.setAdjacentTaskFragment(primary); + primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK); + doReturn(true).when(primary).shouldBoostDimmer(); + task.assignChildLayers(t); + + // The layers are initially assigned via the hierarchy, but the primary will be boosted and + // assigned again to above of the secondary. + verify(primary).assignLayer(t, 0); + verify(secondary).assignLayer(t, 1); + verify(primary).assignLayer(t, 2); + } + + @Test public void testMoveOrCreateDecorSurface() { final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 0514943a83c5..51df1d4cb14d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -195,6 +195,36 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testCreateInfo_Activity() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task theTask = createTask(mDisplayContent); + final ActivityRecord closing = createActivityRecord(theTask); + final ActivityRecord opening = createActivityRecord(theTask); + // Start states. + changes.put(theTask, new Transition.ChangeInfo(theTask, true /* vis */, false /* exChg */)); + changes.put(opening, new Transition.ChangeInfo(opening, false /* vis */, true /* exChg */)); + changes.put(closing, new Transition.ChangeInfo(closing, true /* vis */, false /* exChg */)); + fillChangeMap(changes, theTask); + // End states. + closing.setVisibleRequested(false); + opening.setVisibleRequested(true); + + final int transit = transition.mType; + int flags = 0; + + participants.add(opening); + participants.add(closing); + ArrayList<Transition.ChangeInfo> targets = + Transition.calculateTargets(participants, changes); + TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, mMockT); + assertEquals(2, info.getChanges().size()); + assertEquals(info.getChanges().get(1).getActivityComponent(), closing.mActivityComponent); + } + + @Test public void testCreateInfo_NestedTasks() { final Transition transition = createTestTransition(TRANSIT_OPEN); ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; 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 21fee7286a7b..a1cc8d5d9188 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -80,6 +80,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; import android.view.IWindow; @@ -102,16 +103,19 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.AdoptShellPermissionsRule; import com.android.internal.os.IResultReceiver; import com.android.server.LocalServices; +import com.android.server.wm.SensitiveContentPackages.PackageInfo; import com.android.server.wm.WindowManagerService.WindowContainerInfo; import com.google.common.truth.Expect; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; +import java.util.Collections; /** * Build/Install/Run: @@ -133,6 +137,11 @@ public class WindowManagerServiceTests extends WindowTestsBase { @Rule public Expect mExpect = Expect.create(); + @After + public void tearDown() { + mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + @Test public void testIsRequestedOrientationMapped() { mWm.setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled*/ true, @@ -815,6 +824,42 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + public void setShouldBlockScreenCaptureForApp() { + 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.setShouldBlockScreenCaptureForApp(blockedPackages); + + assertTrue(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); + assertFalse(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId2)); + verify(mWm).refreshScreenCaptureDisabled(); + } + + @Test + public void setShouldBlockScreenCaptureForApp_emptySet_clearsCache() { + String testPackage = "test"; + int ownerId1 = 20; + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages); + wmInternal.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + + assertFalse(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); + verify(mWm, times(2)).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 f24baba9ca0c..fb4edfacb8e3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -55,6 +55,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.notification.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.WindowContainer.SYNC_STATE_WAITING_FOR_DRAW; @@ -90,6 +91,7 @@ import android.os.IBinder; import android.os.InputConfig; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.Gravity; @@ -109,7 +111,9 @@ import android.window.TaskFragmentOrganizer; import androidx.test.filters.SmallTest; import com.android.server.testutils.StubTransaction; +import com.android.server.wm.SensitiveContentPackages.PackageInfo; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -130,6 +134,11 @@ import java.util.List; @RunWith(WindowTestRunner.class) public class WindowStateTests extends WindowTestsBase { + @After + public void tearDown() { + mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + } + @Test public void testIsParentWindowHidden() { final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow"); @@ -1373,6 +1382,28 @@ public class WindowStateTests extends WindowTestsBase { assertThat(listener.mIsVisibleForImeTargetOverlay).isFalse(); } + @Test + @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + public void testIsSecureLocked_sensitiveContentProtectionManagerEnabled() { + String testPackage = "test"; + int ownerId1 = 20; + int ownerId2 = 21; + final WindowState window1 = createWindow(null, TYPE_APPLICATION, "window1", ownerId1); + final WindowState window2 = createWindow(null, TYPE_APPLICATION, "window2", ownerId2); + + // Setting packagename for targeted feature + window1.mAttrs.packageName = testPackage; + window2.mAttrs.packageName = testPackage; + + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedPackages); + + assertTrue(window1.isSecureLocked()); + assertFalse(window2.isSecureLocked()); + } + private static class TestImeTargetChangeListener implements ImeTargetChangeListener { private IBinder mImeTargetToken; private boolean mIsRemoved; diff --git a/telephony/java/android/telephony/euicc/EuiccCardManager.java b/telephony/java/android/telephony/euicc/EuiccCardManager.java index e981e1f92071..69594f27e65c 100644 --- a/telephony/java/android/telephony/euicc/EuiccCardManager.java +++ b/telephony/java/android/telephony/euicc/EuiccCardManager.java @@ -183,6 +183,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and all the profiles. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestAllProfiles(String cardId, @CallbackExecutor Executor executor, ResultCallback<EuiccProfileInfo[]> callback) { @@ -212,6 +215,9 @@ public class EuiccCardManager { * @param iccid The iccid of the profile. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and profile. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestProfile(String cardId, String iccid, @CallbackExecutor Executor executor, ResultCallback<EuiccProfileInfo> callback) { @@ -244,6 +250,9 @@ public class EuiccCardManager { * ICCID is known, an APDU will be sent through to read the enabled profile. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and the profile. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestEnabledProfileForPort(@NonNull String cardId, int portIndex, @NonNull @CallbackExecutor Executor executor, @@ -276,6 +285,9 @@ public class EuiccCardManager { * @param refresh Whether sending the REFRESH command to modem. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void disableProfile(String cardId, String iccid, boolean refresh, @CallbackExecutor Executor executor, ResultCallback<Void> callback) { @@ -307,6 +319,9 @@ public class EuiccCardManager { * @param refresh Whether sending the REFRESH command to modem. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and the EuiccProfileInfo enabled. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @deprecated instead use {@link #switchToProfile(String, String, int, boolean, Executor, * ResultCallback)} */ @@ -344,6 +359,9 @@ public class EuiccCardManager { * @param refresh Whether sending the REFRESH command to modem. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and the EuiccProfileInfo enabled. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void switchToProfile(@Nullable String cardId, @Nullable String iccid, int portIndex, boolean refresh, @NonNull @CallbackExecutor Executor executor, @@ -375,6 +393,9 @@ public class EuiccCardManager { * @param nickname The nickname of the profile. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void setNickname(String cardId, String iccid, String nickname, @CallbackExecutor Executor executor, ResultCallback<Void> callback) { @@ -404,6 +425,9 @@ public class EuiccCardManager { * @param iccid The iccid of the profile. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void deleteProfile(String cardId, String iccid, @CallbackExecutor Executor executor, ResultCallback<Void> callback) { @@ -434,6 +458,9 @@ public class EuiccCardManager { * EuiccCard for details. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void resetMemory(String cardId, @ResetOption int options, @CallbackExecutor Executor executor, ResultCallback<Void> callback) { @@ -462,6 +489,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and the default SM-DP+ address. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestDefaultSmdpAddress(String cardId, @CallbackExecutor Executor executor, ResultCallback<String> callback) { @@ -490,6 +520,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code and the SM-DS address. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestSmdsAddress(String cardId, @CallbackExecutor Executor executor, ResultCallback<String> callback) { @@ -519,6 +552,9 @@ public class EuiccCardManager { * @param defaultSmdpAddress The default SM-DP+ address to set. * @param executor The executor through which the callback should be invoked. * @param callback The callback to get the result code. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void setDefaultSmdpAddress(String cardId, String defaultSmdpAddress, @CallbackExecutor Executor executor, ResultCallback<Void> callback) { @@ -548,6 +584,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the rule authorisation table. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestRulesAuthTable(String cardId, @CallbackExecutor Executor executor, ResultCallback<EuiccRulesAuthTable> callback) { @@ -576,6 +615,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the challenge. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestEuiccChallenge(String cardId, @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) { @@ -604,6 +646,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the info1. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestEuiccInfo1(String cardId, @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) { @@ -632,6 +677,9 @@ public class EuiccCardManager { * @param cardId The Id of the eUICC. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the info2. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void requestEuiccInfo2(String cardId, @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) { @@ -671,6 +719,9 @@ public class EuiccCardManager { * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and a byte array which represents a * {@code AuthenticateServerResponse} defined in GSMA RSP v2.0+. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void authenticateServer(String cardId, String matchingId, byte[] serverSigned1, byte[] serverSignature1, byte[] euiccCiPkIdToBeUsed, byte[] serverCertificate, @@ -716,6 +767,9 @@ public class EuiccCardManager { * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and a byte array which represents a * {@code PrepareDownloadResponse} defined in GSMA RSP v2.0+ + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void prepareDownload(String cardId, @Nullable byte[] hashCc, byte[] smdpSigned2, byte[] smdpSignature2, byte[] smdpCertificate, @CallbackExecutor Executor executor, @@ -753,6 +807,9 @@ public class EuiccCardManager { * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and a byte array which represents a * {@code LoadBoundProfilePackageResponse} defined in GSMA RSP v2.0+. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void loadBoundProfilePackage(String cardId, byte[] boundProfilePackage, @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) { @@ -787,6 +844,9 @@ public class EuiccCardManager { * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and an byte[] which represents a * {@code CancelSessionResponse} defined in GSMA RSP v2.0+. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void cancelSession(String cardId, byte[] transactionId, @CancelReason int reason, @CallbackExecutor Executor executor, ResultCallback<byte[]> callback) { @@ -820,6 +880,9 @@ public class EuiccCardManager { * @param events bits of the event types ({@link EuiccNotification.Event}) to list. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the list of notifications. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void listNotifications(String cardId, @EuiccNotification.Event int events, @CallbackExecutor Executor executor, ResultCallback<EuiccNotification[]> callback) { @@ -850,6 +913,9 @@ public class EuiccCardManager { * @param events bits of the event types ({@link EuiccNotification.Event}) to list. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the list of notifications. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void retrieveNotificationList(String cardId, @EuiccNotification.Event int events, @CallbackExecutor Executor executor, ResultCallback<EuiccNotification[]> callback) { @@ -880,6 +946,9 @@ public class EuiccCardManager { * @param seqNumber the sequence number of the notification. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code and the notification. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void retrieveNotification(String cardId, int seqNumber, @CallbackExecutor Executor executor, ResultCallback<EuiccNotification> callback) { @@ -910,6 +979,9 @@ public class EuiccCardManager { * @param seqNumber the sequence number of the notification. * @param executor The executor through which the callback should be invoked. * @param callback the callback to get the result code. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public void removeNotificationFromList(String cardId, int seqNumber, @CallbackExecutor Executor executor, ResultCallback<Void> callback) { diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index 86fbb04d31b6..09d21083afb1 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -927,6 +927,9 @@ public class EuiccManager { * subscription APIs. * * @return true if embedded subscriptions are currently enabled. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public boolean isEnabled() { // In the future, this may reach out to IEuiccController (if non-null) to check any dynamic @@ -942,6 +945,9 @@ public class EuiccManager { * access to the EID of another eUICC. * * @return the EID. May be null if the eUICC is not ready. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @Nullable public String getEid() { @@ -963,6 +969,8 @@ public class EuiccManager { * @return the status of eUICC OTA. If the eUICC is not ready, * {@link OtaStatus#EUICC_OTA_STATUS_UNAVAILABLE} will be returned. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1014,6 +1022,9 @@ public class EuiccManager { * @param subscription the subscription to download. * @param switchAfterDownload if true, the profile will be activated upon successful download. * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void downloadSubscription(DownloadableSubscription subscription, @@ -1075,6 +1086,9 @@ public class EuiccManager { * @param resolutionExtras Resolution-specific extras depending on the result of the resolution. * For example, this may indicate whether the user has consented or may include the input * they provided. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1111,6 +1125,9 @@ public class EuiccManager { * * @param subscription the subscription which needs metadata filled in * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1142,6 +1159,9 @@ public class EuiccManager { * internal system use only. * * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1163,6 +1183,9 @@ public class EuiccManager { * Returns information about the eUICC chip/device. * * @return the {@link EuiccInfo}. May be null if the eUICC is not ready. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @Nullable public EuiccInfo getEuiccInfo() { @@ -1188,6 +1211,9 @@ public class EuiccManager { * * @param subscriptionId the ID of the subscription to delete. * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void deleteSubscription(int subscriptionId, PendingIntent callbackIntent) { @@ -1251,6 +1277,9 @@ public class EuiccManager { * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission, or the * calling app must be authorized to manage the active subscription on the target eUICC. * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void switchToSubscription(int subscriptionId, PendingIntent callbackIntent) { @@ -1312,6 +1341,9 @@ public class EuiccManager { * {@link SubscriptionInfo#getPortIndex()}. * @param portIndex the index of the port to target for the enabled subscription * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void switchToSubscription(int subscriptionId, int portIndex, @@ -1349,6 +1381,9 @@ public class EuiccManager { * @param subscriptionId the ID of the subscription to update. * @param nickname the new nickname to apply. * @param callbackIntent a PendingIntent to launch when the operation completes. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void updateSubscriptionNickname( @@ -1376,6 +1411,8 @@ public class EuiccManager { * @deprecated From R, callers should specify a flag for specific set of subscriptions to erase * and use {@link #eraseSubscriptions(int, PendingIntent)} instead * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1402,6 +1439,8 @@ public class EuiccManager { * @param options flag indicating specific set of subscriptions to erase * @param callbackIntent a PendingIntent to launch when the operation completes. * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1459,6 +1498,9 @@ public class EuiccManager { * determine whether a country is supported please check {@link #isSupportedCountry}. * * @param supportedCountries is a list of strings contains country ISO codes in uppercase. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1487,6 +1529,9 @@ public class EuiccManager { * determine whether a country is supported please check {@link #isSupportedCountry}. * * @param unsupportedCountries is a list of strings contains country ISO codes in uppercase. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1512,6 +1557,9 @@ public class EuiccManager { * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission. * * @return list of strings contains country ISO codes in uppercase. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1535,6 +1583,9 @@ public class EuiccManager { * {@code android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission. * * @return list of strings contains country ISO codes in uppercase. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1566,6 +1617,9 @@ public class EuiccManager { * @param countryIso should be the ISO-3166 country code is provided in uppercase 2 character * format. * @return whether the given country supports eUICC or not. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. * @hide */ @SystemApi @@ -1630,6 +1684,9 @@ public class EuiccManager { * * @param portIndex is an enumeration of the ports available on the UICC. * @return {@code true} if port is available + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC}. */ public boolean isSimPortAvailable(int portIndex) { try { diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java index 71bb329a7281..551057fc43d1 100644 --- a/telephony/java/android/telephony/ims/ImsMmTelManager.java +++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java @@ -779,6 +779,8 @@ public class ImsMmTelManager implements RegistrationManager { * @see android.telephony.CarrierConfigManager#KEY_CARRIER_VOLTE_AVAILABLE_BOOL * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @return true if the user's setting for advanced calling is enabled, false otherwise. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @@ -827,6 +829,8 @@ public class ImsMmTelManager implements RegistrationManager { * @see #isAdvancedCallingSettingEnabled() * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -865,6 +869,8 @@ public class ImsMmTelManager implements RegistrationManager { * @param capability The IMS MmTel capability to query. * @return {@code true} if the MmTel IMS capability is capable for this subscription, false * otherwise. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @@ -893,6 +899,8 @@ public class ImsMmTelManager implements RegistrationManager { * @param capability The IMS MmTel capability to query. * @return {@code true} if the MmTel IMS capability is available for this subscription, false * otherwise. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -986,6 +994,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @return true if the user’s “Video Calling” setting is currently enabled. */ @RequiresPermission(anyOf = { @@ -1017,6 +1027,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see #isVtSettingEnabled() * @hide */ @@ -1060,6 +1072,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @RequiresPermission(anyOf = { @@ -1090,6 +1104,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @param isEnabled true if the user's setting for Voice over WiFi is enabled, false otherwise= * @see #isVoWiFiSettingEnabled() * @hide @@ -1148,6 +1164,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws ImsException if the IMS service associated with this subscription is not available or * the IMS service is not available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @return true if the user's setting for Voice over Cross SIM is enabled and false if it is not */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). @@ -1192,6 +1210,8 @@ public class ImsMmTelManager implements RegistrationManager { * </ul> * @throws ImsException if the IMS service associated with this subscription is not available or * the IMS service is not available. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @param isEnabled true if the user's setting for Voice over Cross SIM is enabled, * false otherwise * @see #isCrossSimCallingEnabled() @@ -1233,6 +1253,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @return true if the user's setting for Voice over WiFi while roaming is enabled, false * if disabled. */ @@ -1267,6 +1289,8 @@ public class ImsMmTelManager implements RegistrationManager { * false otherwise. * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see #isVoWiFiRoamingSettingEnabled() * @hide */ @@ -1304,6 +1328,8 @@ public class ImsMmTelManager implements RegistrationManager { * - {@link #WIFI_MODE_WIFI_PREFERRED} * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see #setVoWiFiSettingEnabled(boolean) * @hide */ @@ -1347,6 +1373,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @return The Voice over WiFi Mode preference set by the user, which can be one of the * following: * - {@link #WIFI_MODE_WIFI_ONLY} @@ -1386,6 +1414,8 @@ public class ImsMmTelManager implements RegistrationManager { * - {@link #WIFI_MODE_WIFI_PREFERRED} * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see #getVoWiFiModeSetting() * @hide */ @@ -1422,6 +1452,8 @@ public class ImsMmTelManager implements RegistrationManager { * - {@link #WIFI_MODE_WIFI_PREFERRED} * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see #setVoWiFiRoamingSettingEnabled(boolean) * @hide */ @@ -1458,6 +1490,8 @@ public class ImsMmTelManager implements RegistrationManager { * - {@link #WIFI_MODE_WIFI_PREFERRED} * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see #getVoWiFiRoamingModeSetting() * @hide */ @@ -1492,6 +1526,8 @@ public class ImsMmTelManager implements RegistrationManager { * settings. * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @param isEnabled if true RTT should be enabled during calls made on this subscription. * @hide */ @@ -1535,6 +1571,8 @@ public class ImsMmTelManager implements RegistrationManager { * * @throws IllegalArgumentException if the subscription associated with this operation is not * active (SIM is not inserted, ESIM inactive) or invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @see android.telephony.CarrierConfigManager#KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL */ @SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236). diff --git a/telephony/java/android/telephony/ims/ImsRcsManager.java b/telephony/java/android/telephony/ims/ImsRcsManager.java index 2b49bcd4e928..62d426383e57 100644 --- a/telephony/java/android/telephony/ims/ImsRcsManager.java +++ b/telephony/java/android/telephony/ims/ImsRcsManager.java @@ -250,6 +250,8 @@ public class ImsRcsManager { * the {@code ImsService} associated with the subscription is not available. This can happen if * the service crashed, for example. See {@link ImsException#getCode()} for a more detailed * reason. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public void registerImsRegistrationCallback( @@ -294,6 +296,8 @@ public class ImsRcsManager { * @param c The {@link RegistrationManager.RegistrationCallback} to be removed. * @see android.telephony.SubscriptionManager.OnSubscriptionsChangedListener * @see #registerImsRegistrationCallback(Executor, RegistrationCallback) + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public void unregisterImsRegistrationCallback( @@ -329,6 +333,8 @@ public class ImsRcsManager { * following: {@link RegistrationManager#REGISTRATION_STATE_NOT_REGISTERED}, * {@link RegistrationManager#REGISTRATION_STATE_REGISTERING}, or * {@link RegistrationManager#REGISTRATION_STATE_REGISTERED}. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public void getRegistrationState(@NonNull @CallbackExecutor Executor executor, @@ -378,6 +384,8 @@ public class ImsRcsManager { * {@see AccessNetworkConstants#TRANSPORT_TYPE_WWAN}, * {@see AccessNetworkConstants#TRANSPORT_TYPE_WLAN}, or * {@see AccessNetworkConstants#TRANSPORT_TYPE_INVALID}. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public void getRegistrationTransportType(@NonNull @CallbackExecutor Executor executor, @@ -435,6 +443,8 @@ public class ImsRcsManager { * {@link ImsRcsManager} is valid, but the ImsService associated with the subscription is not * available. This can happen if the ImsService has crashed, for example, or if the subscription * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -479,6 +489,8 @@ public class ImsRcsManager { * @see #addOnAvailabilityChangedListener(Executor, OnAvailabilityChangedListener) * @throws ImsException if the IMS service is not available when calling this method. * See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -525,6 +537,8 @@ public class ImsRcsManager { * @see android.telephony.CarrierConfigManager.Ims#KEY_ENABLE_PRESENCE_CAPABILITY_EXCHANGE_BOOL * @throws ImsException if the IMS service is not available when calling this method. * See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -563,6 +577,8 @@ public class ImsRcsManager { * @see #isCapable(int, int) * @throws ImsException if the IMS service is not available when calling this method. * See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi diff --git a/telephony/java/android/telephony/ims/ProvisioningManager.java b/telephony/java/android/telephony/ims/ProvisioningManager.java index 1c5d1e940030..62b8420fc66e 100644 --- a/telephony/java/android/telephony/ims/ProvisioningManager.java +++ b/telephony/java/android/telephony/ims/ProvisioningManager.java @@ -1300,8 +1300,10 @@ public class ProvisioningManager { * @param executor The executor that the callback methods will be called on. * @param callback The callback instance being registered. * @throws ImsException if the subscription associated with this callback is - * valid, but the {@link ImsService the service crashed, for example. See + * valid, but the service crashed, for example. See * {@link ImsException#getCode()} for a more detailed reason. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public void registerFeatureProvisioningChangedCallback( @@ -1327,6 +1329,8 @@ public class ProvisioningManager { * * @param callback The existing {@link FeatureProvisioningCallback} to be removed. * @see #registerFeatureProvisioningChangedCallback(Executor, FeatureProvisioningCallback) + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ public void unregisterFeatureProvisioningChangedCallback( @NonNull FeatureProvisioningCallback callback) { @@ -1347,6 +1351,8 @@ public class ProvisioningManager { * @return an integer value for the provided key, or * {@link ImsConfigImplBase#CONFIG_RESULT_UNKNOWN} if the key doesn't exist. * @throws IllegalArgumentException if the key provided was invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -1369,6 +1375,8 @@ public class ProvisioningManager { * @return a String value for the provided key, {@code null} if the key doesn't exist, or * {@link StringResultError} if there was an error getting the value for the provided key. * @throws IllegalArgumentException if the key provided was invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -1392,6 +1400,8 @@ public class ProvisioningManager { * @param key An integer that represents the provisioning key, which is defined by the OEM. * @param value a integer value for the provided key. * @return the result of setting the configuration value. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide * * Note: For compatibility purposes, the integer values [0 - 99] used in @@ -1420,6 +1430,8 @@ public class ProvisioningManager { * should be appropriately namespaced to avoid collision. * @param value a String value for the provided key. * @return the result of setting the configuration value. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -1451,6 +1463,9 @@ public class ProvisioningManager { * * @see CarrierConfigManager.Ims#KEY_MMTEL_REQUIRES_PROVISIONING_BUNDLE * @param isProvisioned true if the device is provisioned for UT over IMS, false otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @WorkerThread @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -1485,6 +1500,9 @@ public class ProvisioningManager { * @return true if the device is provisioned for the capability or does not require * provisioning, false if the capability does require provisioning and has not been * provisioned yet. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @WorkerThread @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) @@ -1509,6 +1527,9 @@ public class ProvisioningManager { * @return true if the device is provisioned for the capability or does not require * provisioning, false if the capability does require provisioning and has not been * provisioned yet. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. + * * @deprecated Use {@link #getRcsProvisioningStatusForCapability(int, int)} instead, * as this only retrieves provisioning information for * {@link ImsRegistrationImplBase#REGISTRATION_TECH_LTE} @@ -1546,6 +1567,9 @@ public class ProvisioningManager { * @return true if the device is provisioned for the capability or does not require * provisioning, false if the capability does require provisioning and has not been * provisioned yet. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @WorkerThread @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) @@ -1577,6 +1601,9 @@ public class ProvisioningManager { * @see CarrierConfigManager#KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL * @param isProvisioned true if the device is provisioned for the RCS capability specified, * false otherwise. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. + * * @deprecated Use {@link #setRcsProvisioningStatusForCapability(int, int, boolean)} instead, * as this method only sets provisioning information for * {@link ImsRegistrationImplBase#REGISTRATION_TECH_LTE} @@ -1615,6 +1642,9 @@ public class ProvisioningManager { * @see CarrierConfigManager.Ims#KEY_RCS_REQUIRES_PROVISIONING_BUNDLE * @param isProvisioned true if the device is provisioned for the RCS capability specified, * false otherwise. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @WorkerThread @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -1644,6 +1674,9 @@ public class ProvisioningManager { * @return true if provisioning is required for the MMTEL capability and IMS * registration technology specified, false if it is not required or if the device does not * support IMS. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public boolean isProvisioningRequiredForCapability( @@ -1672,6 +1705,9 @@ public class ProvisioningManager { * @return true if provisioning is required for the RCS capability and IMS * registration technology specified, false if it is not required or if the device does not * support IMS. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PRECISE_PHONE_STATE) public boolean isRcsProvisioningRequiredForCapability( @@ -1700,10 +1736,14 @@ public class ProvisioningManager { * @param config The XML file to be read. ASCII/UTF8 encoded text if not compressed. * @param isCompressed The XML file is compressed in gzip format and must be decompressed * before being read. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION) public void notifyRcsAutoConfigurationReceived(@NonNull byte[] config, boolean isCompressed) { if (config == null) { throw new IllegalArgumentException("Must include a non-null config XML file."); @@ -1714,7 +1754,6 @@ public class ProvisioningManager { } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } - } /** @@ -1787,10 +1826,14 @@ public class ProvisioningManager { * When the IMS/RCS service receives the RCS client configuration, it will detect * the change in the configuration, and trigger the auto-configuration as needed. * @param rcc RCS client configuration {@link RcsClientConfiguration} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION) public void setRcsClientConfiguration( @NonNull RcsClientConfiguration rcc) throws ImsException { try { @@ -1826,6 +1869,7 @@ public class ProvisioningManager { @RequiresPermission(anyOf = { Manifest.permission.READ_PRIVILEGED_PHONE_STATE, Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION}) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION) public boolean isRcsVolteSingleRegistrationCapable() throws ImsException { try { return getITelephony().isRcsVolteSingleRegistrationCapable(mSubId); @@ -1870,12 +1914,15 @@ public class ProvisioningManager { * params (See {@link #setRcsClientConfiguration}) and re register the * callback. * See {@link ImsException#getCode()} for a more detailed reason. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. * @hide */ @SystemApi @RequiresPermission(anyOf = { Manifest.permission.READ_PRIVILEGED_PHONE_STATE, Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION}) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION) public void registerRcsProvisioningCallback( @NonNull @CallbackExecutor Executor executor, @NonNull RcsProvisioningCallback callback) throws ImsException { @@ -1908,12 +1955,15 @@ public class ProvisioningManager { * @see #registerRcsProvisioningCallback(Executor, RcsProvisioningCallback) * @throws IllegalArgumentException if the subscription associated with * this callback is invalid. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. * @hide */ @SystemApi @RequiresPermission(anyOf = { Manifest.permission.READ_PRIVILEGED_PHONE_STATE, Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION}) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION) public void unregisterRcsProvisioningCallback( @NonNull RcsProvisioningCallback callback) { try { @@ -1935,10 +1985,14 @@ public class ProvisioningManager { * {@link RcsProvisioningCallback#onConfigurationReset}, then * {@link RcsProvisioningCallback#onConfigurationChanged} when the new * RCS configuration is received and notified by {@link #notifyRcsAutoConfigurationReceived} + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION) public void triggerRcsReconfiguration() { try { getITelephony().triggerRcsReconfiguration(mSubId); diff --git a/telephony/java/android/telephony/ims/RcsUceAdapter.java b/telephony/java/android/telephony/ims/RcsUceAdapter.java index 3bb9be0252cd..8925a9e82942 100644 --- a/telephony/java/android/telephony/ims/RcsUceAdapter.java +++ b/telephony/java/android/telephony/ims/RcsUceAdapter.java @@ -21,9 +21,11 @@ import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Binder; import android.os.IBinder; @@ -49,6 +51,7 @@ import java.util.concurrent.Executor; * * @see ImsRcsManager#getUceAdapter() for information on creating an instance of this class. */ +@RequiresFeature(PackageManager.FEATURE_TELEPHONY_IMS) public class RcsUceAdapter { private static final String TAG = "RcsUceAdapter"; @@ -585,6 +588,8 @@ public class RcsUceAdapter { * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not * available. This can happen if the ImsService has crashed, for example, or if the subscription * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -682,6 +687,8 @@ public class RcsUceAdapter { * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not * available. This can happen if the ImsService has crashed, for example, or if the subscription * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -759,6 +766,8 @@ public class RcsUceAdapter { * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not * available. This can happen if the ImsService has crashed, for example, or if the subscription * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -800,6 +809,8 @@ public class RcsUceAdapter { * the {@link ImsService} associated with the subscription is not available. This can happen if * the service crashed, for example. See {@link ImsException#getCode()} for a more detailed * reason. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -845,6 +856,8 @@ public class RcsUceAdapter { * the {@link ImsService} associated with the subscription is not available. This can happen if * the service crashed, for example. See {@link ImsException#getCode()} for a more detailed * reason. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi @@ -901,6 +914,8 @@ public class RcsUceAdapter { * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not * available. This can happen if the ImsService has crashed, for example, or if the subscription * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. */ @RequiresPermission(Manifest.permission.READ_PHONE_STATE) public boolean isUceSettingEnabled() throws ImsException { @@ -954,6 +969,8 @@ public class RcsUceAdapter { * {@link RcsUceAdapter} is valid, but the ImsService associated with the subscription is not * available. This can happen if the ImsService has crashed, for example, or if the subscription * becomes inactive. See {@link ImsException#getCode()} for more information on the error codes. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS}. * @hide */ @SystemApi diff --git a/telephony/java/android/telephony/ims/SipDelegateManager.java b/telephony/java/android/telephony/ims/SipDelegateManager.java index 25ebdd0b8b40..abf2105327a3 100644 --- a/telephony/java/android/telephony/ims/SipDelegateManager.java +++ b/telephony/java/android/telephony/ims/SipDelegateManager.java @@ -525,6 +525,8 @@ public class SipDelegateManager { * @param callback The callback instance being registered. * @throws ImsException in the case that the callback can not be registered. * See {@link ImsException#getCode} for more information on when this is called. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. */ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void registerSipDialogStateCallback(@NonNull Executor executor, @@ -557,6 +559,9 @@ public class SipDelegateManager { * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE} * * @param callback The callback instance to be unregistered. + * + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION}. */ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void unregisterSipDialogStateCallback(@NonNull SipDialogStateCallback callback) diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 84777c9441a1..9b5ee0cd82f3 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3055,6 +3055,29 @@ interface ITelephony { boolean setEmergencyCallToSatelliteHandoverType(int handoverType, int delaySeconds); /** + * This API should be used by only CTS tests to forcefully set the country codes. + * + * @param reset {@code true} mean the overridden country codes should not be used, {@code false} + * otherwise. + * @return {@code true} if the country code is set successfully, {@code false} otherwise. + */ + boolean setCountryCodes(in boolean reset, in List<String> currentNetworkCountryCodes, + in Map cachedNetworkCountryCodes, in String locationCountryCode, + in long locationCountryCodeTimestampNanos); + + /** + * This API should be used by only CTS tests to override the overlay configs of satellite + * access controller. + * + * @param reset {@code true} mean the overridden configs should not be used, {@code false} + * otherwise. + * @return {@code true} if the overlay configs are set successfully, {@code false} otherwise. + */ + boolean setSatelliteAccessControlOverlayConfigs(in boolean reset, in boolean isAllowed, + in String s2CellFile, in long locationFreshDurationNanos, + in List<String> satelliteCountryCodes); + + /** * Test method to confirm the file contents are not altered. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java index ba9e4a831789..f82d9ca13938 100644 --- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java +++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java @@ -130,14 +130,13 @@ public class UpdatableSystemFontTest { private static final Pattern PATTERN_SYSTEM_FONT_FILES = Pattern.compile("^/(system|product)/fonts/"); - private String mKeyId; private FontManager mFontManager; private UiDevice mUiDevice; @Before public void setUp() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - mKeyId = insertCert(CERT_PATH); + insertCert(CERT_PATH); mFontManager = context.getSystemService(FontManager.class); expectCommandToSucceed("cmd font clear"); mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); @@ -147,9 +146,6 @@ public class UpdatableSystemFontTest { public void tearDown() throws Exception { // Ignore errors because this may fail if updatable system font is not enabled. runShellCommand("cmd font clear", null); - if (mKeyId != null) { - expectCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity"); - } } @Test @@ -369,20 +365,11 @@ public class UpdatableSystemFontTest { assertThat(isFileOpenedBy(fontPath, EMOJI_RENDERING_TEST_APP_ID)).isFalse(); } - private static String insertCert(String certPath) throws Exception { - Pair<String, String> result; - try (InputStream is = new FileInputStream(certPath)) { - result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is); - } + private static void insertCert(String certPath) throws Exception { // /data/local/tmp is not readable by system server. Copy a cert file to /data/fonts final String copiedCert = "/data/fonts/debug_cert.der"; runShellCommand("cp " + certPath + " " + copiedCert, null); runShellCommand("cmd font install-debug-cert " + copiedCert, null); - // Assert that there are no errors. - assertThat(result.second).isEmpty(); - String keyId = result.first.trim(); - assertThat(keyId).matches("^\\d+$"); - return keyId; } private int updateFontFile(String fontPath, String signaturePath) throws IOException { |