diff options
269 files changed, 7610 insertions, 1640 deletions
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/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..e0b224e92a04 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 @@ -26291,6 +26293,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 @@ -53974,6 +53977,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/test-current.txt b/core/api/test-current.txt index 572be192fb3e..d2af9db3714c 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); @@ -3007,6 +3017,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/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/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..f0a89960cad1 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; @@ -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/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..0b60977e48c7 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -131,3 +131,10 @@ 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" +} 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/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/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/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/provider/Settings.java b/core/java/android/provider/Settings.java index 84fddcb2c22a..144e64f9c27b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -108,10 +108,10 @@ 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; @@ -3585,10 +3585,12 @@ public final class Settings { || applicationInfo.isSignedWithPlatformKey(); } - public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix, - List<String> names) { + private ArrayMap<String, String> getStringsForPrefixStripPrefix( + ContentResolver cr, String prefix, 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; @@ -3613,10 +3615,13 @@ public final class Settings { if (DEBUG) { Log.i(TAG, "Cache hit for prefix:" + prefix); } - if (!names.isEmpty()) { + if (names.length > 0) { for (String name : names) { - if (mValues.containsKey(name)) { - keyValues.put(name, mValues.get(name)); + String value = mValues.get(name); + if (value != null) { + keyValues.put( + name.substring(substringLength), + value); } } } else { @@ -3625,7 +3630,10 @@ 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, mValues.get(key)); + String value = mValues.valueAt(i); + keyValues.put( + key.substring(substringLength), + value); } } } @@ -3685,14 +3693,22 @@ 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.isEmpty()) { - for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { - if (names.contains(flag.getKey())) { - keyValues.put(flag.getKey(), flag.getValue()); + if (names.length > 0) { + for (String name : names) { + String value = flagsToValues.get(name); + if (value != null) { + keyValues.put( + name.substring(substringLength), + value); } } } else { - keyValues.putAll(flagsToValues); + keyValues.ensureCapacity(keyValues.size() + flagsToValues.size()); + for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { + keyValues.put( + flag.getKey().substring(substringLength), + flag.getValue()); + } } synchronized (NameValueCache.this) { @@ -12172,6 +12188,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 */ @@ -19674,6 +19691,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"; } } @@ -19834,21 +19860,15 @@ 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) { - List<String> compositeNames = new ArrayList<>(names.size()); - for (String name : names) { - compositeNames.add(createCompositeName(namespace, name)); + String[] compositeNames = new String[names.size()]; + for (int i = 0, size = names.size(); i < size; ++i) { + compositeNames[i] = createCompositeName(namespace, names.get(i)); } String prefix = createPrefix(namespace); - ArrayMap<String, String> rawKeyValues = sNameValueCache.getStringsForPrefix( + + ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix( 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; } @@ -20174,12 +20194,13 @@ public final class Settings { private static String createCompositeName(@NonNull String namespace, @NonNull String name) { Preconditions.checkNotNull(namespace); Preconditions.checkNotNull(name); - return createPrefix(namespace) + name; + var sb = new StringBuilder(namespace.length() + 1 + name.length()); + return sb.append(namespace).append('/').append(name).toString(); } private static String createPrefix(@NonNull String namespace) { Preconditions.checkNotNull(namespace); - return namespace + "/"; + return namespace + '/'; } private static Uri createNamespaceUri(@NonNull String namespace) { 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/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..b4ac9a22aad3 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -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/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/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/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/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/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_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/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_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/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/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/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/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/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..89dcc4c1d261 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); @@ -1022,7 +1017,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 +1068,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 +1097,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 +1241,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/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/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/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/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/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/SettingsTheme/res/values-v31/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml index f44b16104f99..0e40db23c66c 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v31/styles.xml @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -<resources> +<resources + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> <style name="TextAppearance.PreferenceTitle.SettingsLib" parent="@android:style/TextAppearance.Material.Subhead"> - <item name="android:textColor">@color/settingslib_text_color_primary</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> <item name="android:fontFamily">@string/settingslib_config_headlineFontFamily</item> <item name="android:textSize">20sp</item> </style> 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/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/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..d20154437e02 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,7 @@ 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.Widgets import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults @@ -52,6 +53,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,6 +74,7 @@ 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.IntSize import androidx.compose.ui.unit.LayoutDirection @@ -100,7 +103,8 @@ fun CommunalHub( 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 +115,8 @@ fun CommunalHub( isDraggingToRemove = checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates) isDraggingToRemove - } + }, + onOpenWidgetPicker = onOpenWidgetPicker, ) if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) { @@ -148,13 +153,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 = @@ -207,17 +213,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 +263,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 +276,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 +319,20 @@ private fun Toolbar( } @Composable -private fun filledButtonColors(): ButtonColors { - val colors = LocalAndroidColorScheme.current - return ButtonDefaults.buttonColors( - containerColor = colors.primary, - contentColor = colors.onPrimary, +private fun RemoveButtonContent(spacerModifier: Modifier) { + Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_open_widget_editor)) + 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 +342,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 +368,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 +629,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/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/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..4a935d0e229a 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 @@ -91,7 +91,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test - fun communalContent_onlyWidgetsAreShownInEditMode() = + fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) @@ -119,16 +119,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 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..16e0bc00ad35 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 @@ -112,7 +112,7 @@ class CommunalViewModelTest : SysuiTestCase() { } @Test - fun ordering_smartspaceBeforeUmoBeforeWidgets() = + fun ordering_smartspaceBeforeUmoBeforeWidgetsBeforeCtaTile() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) @@ -140,12 +140,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 +156,7 @@ 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) } } 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/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/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/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..64a1d248b221 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 diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 854bb0f05c5e..3f11faebffb1 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1079,6 +1079,14 @@ <!-- 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> <!-- 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/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/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 18fb895f4aaf..827123882c74 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,6 +96,9 @@ constructor( editWidgetsActivityStarter.startActivity() } + /** Dismiss the CTA tile from the hub in view mode. */ + fun dismissCtaTile() = communalRepository.setCtaTileInViewModeVisibility(isVisible = false) + /** Add a widget at the specified position. */ fun addWidget(componentName: ComponentName, priority: Int) = widgetRepository.addWidget(componentName, priority) @@ -125,27 +126,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 +158,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/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..4fabd97531b1 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 @@ -103,6 +103,9 @@ 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 } 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..d8e831c5a40d 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 @@ -28,6 +28,7 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Provider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -42,9 +43,11 @@ constructor( 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) 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..066e897cdfdb 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 @@ -54,15 +54,18 @@ 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 } } override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() + override fun onDismissCtaTile() = communalInteractor.dismissCtaTile() + override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler } 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..a6f71c29b380 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -94,8 +94,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 +144,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 +189,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 +200,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 +263,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 +277,7 @@ public final class NotificationAttentionHelper { } private void loadUserSettings() { - if (mEnablePoliteNotificationsFeature) { + if (Flags.politeNotifications()) { try { mCurrentWorkProfileId = getManagedProfileId(ActivityManager.getCurrentUser()); @@ -301,11 +298,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 +482,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; @@ -618,7 +618,7 @@ public final class NotificationAttentionHelper { private boolean isPoliteNotificationFeatureEnabled(final NotificationRecord record) { // Check feature flag - if (!mEnablePoliteNotificationsFeature) { + if (!Flags.politeNotifications()) { return false; } @@ -1064,9 +1064,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 +1080,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 +1153,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 +1222,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 +1277,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 +1433,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 +1458,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..9ed3559c1389 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2025,6 +2025,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 +2139,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 +2161,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; diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 3244aff2a4dc..db64a750678b 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -893,48 +893,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 +1100,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 +1197,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, @@ -2023,6 +2229,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/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/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/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/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 5c603c2238bd..5d716fc46f7a 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); } 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 03e263a17d2b..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) { 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/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/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/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 6fffdd010028..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); @@ -115,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); 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/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/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/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/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..97e94e3c6fc9 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,9 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; +import android.hardware.biometrics.BiometricManager; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -53,7 +56,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 +91,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 +132,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 +168,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()); @@ -322,6 +345,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) { @@ -378,6 +468,11 @@ public class TrustManagerServiceTest { scheduler); } + @Override + public void sendBroadcastAsUser(Intent intent, UserHandle user, + @Nullable String receiverPermission, @Nullable Bundle options) { + } + void sendPackageChangedBroadcast(ComponentName changedComponent) { Intent intent = new Intent( Intent.ACTION_PACKAGE_CHANGED, 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/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/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index cf8548cfe689..1b77b99e7d3e 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; @@ -195,7 +199,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 +369,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 +442,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; @@ -1990,7 +2005,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 +2029,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 +2044,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 +2103,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 +2210,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 +2280,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 +2320,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..9eed974c701f 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; @@ -2197,7 +2200,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 +2225,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 +2307,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 +2320,13 @@ 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 @@ -3321,6 +3339,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 +3354,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 +3396,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 be305931c95c..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; @@ -145,6 +146,25 @@ public class SyncEngineTests extends WindowTestsBase { } @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 public void testInvisibleSyncCallback() { TestWindowContainer mockWC = new TestWindowContainer(mWm, true /* waiter */); 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/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 { |