diff options
246 files changed, 8458 insertions, 1265 deletions
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 ed4b92a7030b..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(); @@ -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 @@ -53975,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/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index f9ab55e00dc6..5b354fc3b9ed 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -23,6 +23,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.NotificationManager.InterruptionFilter; import android.content.ComponentName; import android.net.Uri; @@ -35,6 +36,7 @@ import android.view.WindowInsetsController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Objects; /** @@ -111,6 +113,30 @@ public final class AutomaticZenRule implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface Type {} + /** Used to track which rule variables have been modified by the user. + * Should be checked against the bitmask {@link #getUserModifiedFields()}. + * @hide + */ + @IntDef(flag = true, prefix = { "FIELD_" }, value = { + FIELD_NAME, + FIELD_INTERRUPTION_FILTER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ModifiableField {} + + /** + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int FIELD_NAME = 1 << 0; + /** + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int FIELD_INTERRUPTION_FILTER = 1 << 1; + private boolean enabled; private String name; private @InterruptionFilter int interruptionFilter; @@ -120,12 +146,14 @@ public final class AutomaticZenRule implements Parcelable { private long creationTime; private ZenPolicy mZenPolicy; private ZenDeviceEffects mDeviceEffects; + // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined. private boolean mModified = false; private String mPkg; - private int mType = TYPE_UNKNOWN; + private int mType = Flags.modesApi() ? TYPE_UNKNOWN : 0; private int mIconResId; private String mTriggerDescription; private boolean mAllowManualInvocation; + private @ModifiableField int mUserModifiedFields; // Bitwise representation /** * The maximum string length for any string contained in this automatic zen rule. This pertains @@ -228,6 +256,7 @@ public final class AutomaticZenRule implements Parcelable { mIconResId = source.readInt(); mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); mType = source.readInt(); + mUserModifiedFields = source.readInt(); } } @@ -278,6 +307,8 @@ public final class AutomaticZenRule implements Parcelable { * Returns whether this rule's name has been modified by the user. * @hide */ + // TODO: b/310620812 - Replace with mUserModifiedFields & FIELD_NAME once + // FLAG_MODES_API is inlined. public boolean isModified() { return mModified; } @@ -475,6 +506,32 @@ public final class AutomaticZenRule implements Parcelable { return type; } + /** + * Gets the bitmask representing which fields are user modified. Bits are set using + * {@link ModifiableField}. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public @ModifiableField int getUserModifiedFields() { + return mUserModifiedFields; + } + + /** + * Returns {@code true} if the {@link AutomaticZenRule} can be updated. + * When this returns {@code false}, calls to + * {@link NotificationManager#updateAutomaticZenRule(String, AutomaticZenRule)}) with this rule + * will ignore changes to user-configurable fields. + */ + @FlaggedApi(Flags.FLAG_MODES_API) + public boolean canUpdate() { + // The rule is considered updateable if its bitmask has no user modifications, and + // the bitmasks of the policy and device effects have no modification. + return mUserModifiedFields == 0 + && (mZenPolicy == null || mZenPolicy.getUserModifiedFields() == 0) + && (mDeviceEffects == null || mDeviceEffects.getUserModifiedFields() == 0); + } + @Override public int describeContents() { return 0; @@ -503,6 +560,7 @@ public final class AutomaticZenRule implements Parcelable { dest.writeInt(mIconResId); dest.writeString(mTriggerDescription); dest.writeInt(mType); + dest.writeInt(mUserModifiedFields); } } @@ -524,12 +582,26 @@ public final class AutomaticZenRule implements Parcelable { .append(",allowManualInvocation=").append(mAllowManualInvocation) .append(",iconResId=").append(mIconResId) .append(",triggerDescription=").append(mTriggerDescription) - .append(",type=").append(mType); + .append(",type=").append(mType) + .append(",userModifiedFields=") + .append(modifiedFieldsToString(mUserModifiedFields)); } return sb.append(']').toString(); } + @FlaggedApi(Flags.FLAG_MODES_API) + private String modifiedFieldsToString(int bitmask) { + ArrayList<String> modified = new ArrayList<>(); + if ((bitmask & FIELD_NAME) != 0) { + modified.add("FIELD_NAME"); + } + if ((bitmask & FIELD_INTERRUPTION_FILTER) != 0) { + modified.add("FIELD_INTERRUPTION_FILTER"); + } + return "{" + String.join(",", modified) + "}"; + } + @Override public boolean equals(@Nullable Object o) { if (!(o instanceof AutomaticZenRule)) return false; @@ -551,7 +623,8 @@ public final class AutomaticZenRule implements Parcelable { && other.mAllowManualInvocation == mAllowManualInvocation && other.mIconResId == mIconResId && Objects.equals(other.mTriggerDescription, mTriggerDescription) - && other.mType == mType; + && other.mType == mType + && other.mUserModifiedFields == mUserModifiedFields; } return finalEquals; } @@ -561,7 +634,8 @@ public final class AutomaticZenRule implements Parcelable { if (Flags.modesApi()) { return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime, - mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType); + mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType, + mUserModifiedFields); } return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mModified, creationTime, mPkg); @@ -630,6 +704,7 @@ public final class AutomaticZenRule implements Parcelable { private boolean mAllowManualInvocation; private long mCreationTime; private String mPkg; + private @ModifiableField int mUserModifiedFields; public Builder(@NonNull AutomaticZenRule rule) { mName = rule.getName(); @@ -646,6 +721,7 @@ public final class AutomaticZenRule implements Parcelable { mAllowManualInvocation = rule.isManualInvocationAllowed(); mCreationTime = rule.getCreationTime(); mPkg = rule.getPackageName(); + mUserModifiedFields = rule.mUserModifiedFields; } public Builder(@NonNull String name, @NonNull Uri conditionId) { @@ -772,6 +848,19 @@ public final class AutomaticZenRule implements Parcelable { return this; } + /** + * Sets the bitmask representing which fields have been user-modified. + * This method should not be used outside of tests. The value of userModifiedFields + * should be set based on what values are changed when a rule is populated or updated.. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { + mUserModifiedFields = userModifiedFields; + return this; + } + public @NonNull AutomaticZenRule build() { AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity, mConditionId, mPolicy, mInterruptionFilter, mEnabled); @@ -782,6 +871,7 @@ public final class AutomaticZenRule implements Parcelable { rule.mIconResId = mIconResId; rule.mAllowManualInvocation = mAllowManualInvocation; rule.setPackageName(mPkg); + rule.mUserModifiedFields = mUserModifiedFields; return rule; } diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index ac9c497f2a36..d1e517bbd03c 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -30,6 +30,7 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; @@ -577,6 +578,26 @@ public abstract class ForegroundServiceTypePolicy { ); /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PROCESSING = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission( + Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING) + }, true), + null /* anyOfPermissions */, + null /* permissionEnforcementFlag */, + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ + ); + + /** * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}. * * @hide @@ -1331,6 +1352,8 @@ public abstract class ForegroundServiceTypePolicy { FGS_TYPE_POLICY_SYSTEM_EXEMPTED); mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, FGS_TYPE_POLICY_SHORT_SERVICE); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, + FGS_TYPE_POLICY_MEDIA_PROCESSING); // TODO (b/271950506): revisit it in the next release. // Hide the file management type for now. If anyone uses it, will default to "none". mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SPECIAL_USE, diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index cdb92acc5256..843158c0e9fb 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -71,6 +71,12 @@ public final class AssociationInfo implements Parcelable { * @see CompanionDeviceManager#disassociate(int) */ private final boolean mRevoked; + /** + * Indicates that the association is waiting for its corresponding companion app to be installed + * before it can be added to CDM. This is likely because it was restored onto the device from a + * backup. + */ + private final boolean mPending; private final long mTimeApprovedMs; /** * A long value indicates the last time connected reported by selfManaged devices @@ -88,7 +94,7 @@ public final class AssociationInfo implements Parcelable { @Nullable String tag, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, - boolean notifyOnDeviceNearby, boolean revoked, long timeApprovedMs, + boolean notifyOnDeviceNearby, boolean revoked, boolean pending, long timeApprovedMs, long lastTimeConnectedMs, int systemDataSyncFlags) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); @@ -109,6 +115,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = selfManaged; mNotifyOnDeviceNearby = notifyOnDeviceNearby; mRevoked = revoked; + mPending = pending; mTimeApprovedMs = timeApprovedMs; mLastTimeConnectedMs = lastTimeConnectedMs; mSystemDataSyncFlags = systemDataSyncFlags; @@ -236,6 +243,15 @@ public final class AssociationInfo implements Parcelable { } /** + * @return true if the association is waiting for its corresponding app to be installed + * before it can be added to CDM. + * @hide + */ + public boolean isPending() { + return mPending; + } + + /** * @return the last time self reported disconnected for selfManaged only. * @hide */ @@ -318,6 +334,7 @@ public final class AssociationInfo implements Parcelable { + ", mAssociatedDevice=" + mAssociatedDevice + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby + ", mRevoked=" + mRevoked + + ", mPending=" + mPending + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs) + ", mLastTimeConnectedMs=" + ( mLastTimeConnectedMs == Long.MAX_VALUE @@ -336,6 +353,7 @@ public final class AssociationInfo implements Parcelable { && mSelfManaged == that.mSelfManaged && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby && mRevoked == that.mRevoked + && mPending == that.mPending && mTimeApprovedMs == that.mTimeApprovedMs && mLastTimeConnectedMs == that.mLastTimeConnectedMs && Objects.equals(mPackageName, that.mPackageName) @@ -351,7 +369,7 @@ public final class AssociationInfo implements Parcelable { public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName, mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, - mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags); + mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags); } @Override @@ -372,6 +390,7 @@ public final class AssociationInfo implements Parcelable { dest.writeBoolean(mSelfManaged); dest.writeBoolean(mNotifyOnDeviceNearby); dest.writeBoolean(mRevoked); + dest.writeBoolean(mPending); dest.writeLong(mTimeApprovedMs); dest.writeLong(mLastTimeConnectedMs); dest.writeInt(mSystemDataSyncFlags); @@ -389,6 +408,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = in.readBoolean(); mNotifyOnDeviceNearby = in.readBoolean(); mRevoked = in.readBoolean(); + mPending = in.readBoolean(); mTimeApprovedMs = in.readLong(); mLastTimeConnectedMs = in.readLong(); mSystemDataSyncFlags = in.readInt(); @@ -427,6 +447,7 @@ public final class AssociationInfo implements Parcelable { private boolean mSelfManaged; private boolean mNotifyOnDeviceNearby; private boolean mRevoked; + private boolean mPending; private long mTimeApprovedMs; private long mLastTimeConnectedMs; private int mSystemDataSyncFlags; @@ -453,6 +474,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = info.mSelfManaged; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; mRevoked = info.mRevoked; + mPending = info.mPending; mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; @@ -476,6 +498,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged = info.mSelfManaged; mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby; mRevoked = info.mRevoked; + mPending = info.mPending; mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; @@ -549,6 +572,14 @@ public final class AssociationInfo implements Parcelable { } /** @hide */ + @NonNull + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setPending(boolean pending) { + mPending = pending; + return this; + } + + /** @hide */ @TestApi @NonNull @SuppressLint("MissingGetterMatchingBuilder") @@ -606,6 +637,7 @@ public final class AssociationInfo implements Parcelable { mSelfManaged, mNotifyOnDeviceNearby, mRevoked, + mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index ee1d117bf71c..d5eee63fee12 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -8099,7 +8099,7 @@ public class Intent implements Parcelable, Cloneable { int end = data.indexOf('/', 14); if (end < 0) { // All we have is a package name. - intent.mPackage = data.substring(14); + intent.mPackage = Uri.decodeIfNeeded(data.substring(14)); if (!explicitAction) { intent.setAction(ACTION_MAIN); } @@ -8107,21 +8107,22 @@ public class Intent implements Parcelable, Cloneable { } else { // Target the Intent at the given package name always. String authority = null; - intent.mPackage = data.substring(14, end); + intent.mPackage = Uri.decodeIfNeeded(data.substring(14, end)); int newEnd; if ((end+1) < data.length()) { if ((newEnd=data.indexOf('/', end+1)) >= 0) { // Found a scheme, remember it. - scheme = data.substring(end+1, newEnd); + scheme = Uri.decodeIfNeeded(data.substring(end + 1, newEnd)); end = newEnd; if (end < data.length() && (newEnd=data.indexOf('/', end+1)) >= 0) { // Found a authority, remember it. - authority = data.substring(end+1, newEnd); + authority = Uri.decodeIfNeeded( + data.substring(end + 1, newEnd)); end = newEnd; } } else { // All we have is a scheme. - scheme = data.substring(end+1); + scheme = Uri.decodeIfNeeded(data.substring(end + 1)); } } if (scheme == null) { @@ -11762,27 +11763,33 @@ public class Intent implements Parcelable, Cloneable { + this); } uri.append("android-app://"); - uri.append(mPackage); + uri.append(Uri.encode(mPackage)); String scheme = null; if (mData != null) { - scheme = mData.getScheme(); + // All values here must be wrapped with Uri#encodeIfNotEncoded because it is + // possible to exploit the Uri API to return a raw unencoded value, which will + // not deserialize properly and may cause the resulting Intent to be transformed + // to a malicious value. + scheme = Uri.encodeIfNotEncoded(mData.getScheme(), null); if (scheme != null) { uri.append('/'); uri.append(scheme); - String authority = mData.getEncodedAuthority(); + String authority = Uri.encodeIfNotEncoded(mData.getEncodedAuthority(), null); if (authority != null) { uri.append('/'); uri.append(authority); - String path = mData.getEncodedPath(); + + // Multiple path segments are allowed, don't encode the path / separator + String path = Uri.encodeIfNotEncoded(mData.getEncodedPath(), "/"); if (path != null) { uri.append(path); } - String queryParams = mData.getEncodedQuery(); + String queryParams = Uri.encodeIfNotEncoded(mData.getEncodedQuery(), null); if (queryParams != null) { uri.append('?'); uri.append(queryParams); } - String fragment = mData.getEncodedFragment(); + String fragment = Uri.encodeIfNotEncoded(mData.getEncodedFragment(), null); if (fragment != null) { uri.append('#'); uri.append(fragment); diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 30871e938e68..9fe8af516694 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -23,7 +23,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.Activity; -import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; @@ -37,7 +36,6 @@ import android.content.res.TypedArray; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; -import android.os.UserHandle; import android.util.ArraySet; import android.util.Printer; import android.window.OnBackInvokedCallback; @@ -1790,8 +1788,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * @hide */ public boolean isChangeEnabled(long changeId) { - return CompatChanges.isChangeEnabled(changeId, applicationInfo.packageName, - UserHandle.getUserHandleForUid(applicationInfo.uid)); + return applicationInfo.isChangeEnabled(changeId); } /** @hide */ diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 869c621e8564..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/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 0e131b413d0c..433226413917 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -672,6 +672,13 @@ public class PackageInstaller { public @interface UserActionReason {} /** + * The unarchival status is not set. + * + * @hide + */ + public static final int UNARCHIVAL_STATUS_UNSET = -1; + + /** * The unarchival is possible and will commence. * * <p> Note that this does not mean that the unarchival has completed. This status should be @@ -736,6 +743,7 @@ public class PackageInstaller { * @hide */ @IntDef(value = { + UNARCHIVAL_STATUS_UNSET, UNARCHIVAL_OK, UNARCHIVAL_ERROR_USER_ACTION_NEEDED, UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE, @@ -2696,8 +2704,6 @@ public class PackageInstaller { public int developmentInstallFlags = 0; /** {@hide} */ public int unarchiveId = -1; - /** {@hide} */ - public IntentSender unarchiveIntentSender; private final ArrayMap<String, Integer> mPermissionStates; @@ -2750,7 +2756,6 @@ public class PackageInstaller { applicationEnabledSettingPersistent = source.readBoolean(); developmentInstallFlags = source.readInt(); unarchiveId = source.readInt(); - unarchiveIntentSender = source.readParcelable(null, IntentSender.class); } /** {@hide} */ @@ -2785,7 +2790,6 @@ public class PackageInstaller { ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; ret.developmentInstallFlags = developmentInstallFlags; ret.unarchiveId = unarchiveId; - ret.unarchiveIntentSender = unarchiveIntentSender; return ret; } @@ -3495,7 +3499,6 @@ public class PackageInstaller { applicationEnabledSettingPersistent); pw.printHexPair("developmentInstallFlags", developmentInstallFlags); pw.printPair("unarchiveId", unarchiveId); - pw.printPair("unarchiveIntentSender", unarchiveIntentSender); pw.println(); } @@ -3540,7 +3543,6 @@ public class PackageInstaller { dest.writeBoolean(applicationEnabledSettingPersistent); dest.writeInt(developmentInstallFlags); dest.writeInt(unarchiveId); - dest.writeParcelable(unarchiveIntentSender, flags); } public static final Parcelable.Creator<SessionParams> diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 4d704c34195f..ae46c027505e 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -17,6 +17,7 @@ package android.content.pm; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.RequiresPermission; import android.os.Parcel; @@ -471,6 +472,17 @@ public class ServiceInfo extends ComponentInfo public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 1 << 12; /** + * Constant corresponding to {@code mediaProcessing} in + * the {@link android.R.attr#foregroundServiceType} attribute. + * Media processing use cases such as video or photo editing and processing. + */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING + ) + @FlaggedApi(Flags.FLAG_INTRODUCE_MEDIA_PROCESSING_TYPE) + public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING = 1 << 13; + + /** * Constant corresponding to {@code specialUse} in * the {@link android.R.attr#foregroundServiceType} attribute. * Use cases that can't be categorized into any other foreground service types, but also @@ -554,6 +566,7 @@ public class ServiceInfo extends ComponentInfo FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT, + FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING, FOREGROUND_SERVICE_TYPE_SPECIAL_USE, }) @Retention(RetentionPolicy.SOURCE) @@ -640,6 +653,8 @@ public class ServiceInfo extends ComponentInfo return "shortService"; case FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT: return "fileManagement"; + case FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING: + return "mediaProcessing"; case FOREGROUND_SERVICE_TYPE_SPECIAL_USE: return "specialUse"; default: diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 94bec3562bb0..a2cd3e153b3e 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -131,3 +131,18 @@ flag { bug: "310801107" is_fixed_read_only: true } + +flag { + name: "introduce_media_processing_type" + namespace: "backstage_power" + description: "Add a new FGS type for media processing use cases." + bug: "317788011" +} + +flag { + name: "encode_app_intent" + namespace: "package_manager_service" + description: "Feature flag to encode app intent." + bug: "281848623" +} + diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index 9e09759a4282..56f69a67c0b5 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -450,11 +450,14 @@ public final class VirtualDisplayConfig implements Parcelable { * automatically launched upon the display creation. If unset or set to {@code false}, the * display will not host any activities upon creation.</p> * - * <p>Note: setting to {@code true} requires the display to be trusted. If the display is - * not trusted, this property is ignored.</p> + * <p>Note: setting to {@code true} requires the display to be trusted and to not mirror + * content of other displays. If the display is not trusted, or if it mirrors content of + * other displays, this property is ignored.</p> * * @param isHomeSupported whether home activities are supported on the display * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED + * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR + * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY * @hide */ @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME) diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 70de477e9e2e..05a3e182135c 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Intent; +import android.content.pm.Flags; import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; @@ -1971,6 +1972,42 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } /** + * Encodes a value it wasn't already encoded. + * + * @param value string to encode + * @param allow characters to allow + * @return encoded value + * @hide + */ + public static String encodeIfNotEncoded(@Nullable String value, @Nullable String allow) { + if (value == null) return null; + if (!Flags.encodeAppIntent() || isEncoded(value, allow)) return value; + return encode(value, allow); + } + + /** + * Returns true if the given string is already encoded to safe characters. + * + * @param value string to check + * @param allow characters to allow + * @return true if the string is already encoded or false if it should be encoded + */ + private static boolean isEncoded(@Nullable String value, @Nullable String allow) { + if (value == null) return true; + for (int index = 0; index < value.length(); index++) { + char c = value.charAt(index); + + // Allow % because that's the prefix for an encoded character. This method will fail + // for decoded strings whose onlyinvalid character is %, but it's assumed that % alone + // cannot cause malicious behavior in the framework. + if (!isAllowed(c, allow) && c != '%') { + return false; + } + } + return true; + } + + /** * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. * Replaces invalid octets with the unicode replacement character * ("\\uFFFD"). @@ -1988,6 +2025,18 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } /** + * Decodes a string if it was encoded, indicated by containing a %. + * @param value encoded string to decode + * @return decoded value + * @hide + */ + public static String decodeIfNeeded(@Nullable String value) { + if (value == null) return null; + if (Flags.encodeAppIntent() && value.contains("%")) return decode(value); + return value; + } + + /** * Support for part implementations. */ static abstract class AbstractPart { diff --git a/core/java/android/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/provider/Settings.java b/core/java/android/provider/Settings.java index 211bdef89247..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) { @@ -19675,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"; } } @@ -19835,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; } @@ -20175,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/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/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/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 43683ffad432..ce2543a47cf5 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -56,6 +56,7 @@ <permission name="android.permission.REAL_GET_TASKS"/> <permission name="android.permission.REQUEST_NETWORK_SCORES"/> <permission name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE"/> + <permission name="android.permission.SATELLITE_COMMUNICATION"/> <permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/> <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" /> <permission name="android.permission.START_ACTIVITY_AS_CALLER"/> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index b9efe65d2754..a1ea2b8ce032 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -134,6 +134,7 @@ applications that come with the platform <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/> <permission name="android.permission.DUMP"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> + <permission name="android.permission.LOCATION_BYPASS"/> <permission name="android.permission.LOCAL_MAC_ADDRESS"/> <permission name="android.permission.MANAGE_USERS"/> <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS" /> @@ -149,6 +150,7 @@ applications that come with the platform <permission name="android.permission.REGISTER_CALL_PROVIDER"/> <permission name="android.permission.REGISTER_SIM_SUBSCRIPTION"/> <permission name="android.permission.REGISTER_STATS_PULL_ATOM"/> + <permission name="android.permission.SATELLITE_COMMUNICATION"/> <permission name="android.permission.SEND_RESPOND_VIA_MESSAGE"/> <permission name="android.permission.SHUTDOWN"/> <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/> diff --git a/data/keyboards/Vendor_0957_Product_0031.kl b/data/keyboards/Vendor_0957_Product_0031.kl new file mode 100644 index 000000000000..b47ee58c0c10 --- /dev/null +++ b/data/keyboards/Vendor_0957_Product_0031.kl @@ -0,0 +1,82 @@ +# Copyright 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Key Layout file for Google Reference RCU Remote with customizable button. +# + +key 116 TV_POWER WAKE +key 217 ASSIST WAKE +key 423 MACRO_1 WAKE + +key 103 DPAD_UP +key 108 DPAD_DOWN +key 105 DPAD_LEFT +key 106 DPAD_RIGHT +key 353 DPAD_CENTER + +key 158 BACK +key 172 HOME WAKE + +key 113 VOLUME_MUTE +key 114 VOLUME_DOWN +key 115 VOLUME_UP + +key 2 1 +key 3 2 +key 4 3 +key 5 4 +key 6 5 +key 7 6 +key 8 7 +key 9 8 +key 10 9 +key 11 0 + +# custom keys +key usage 0x000c01BB TV_INPUT + +key usage 0x000c0185 TV_TELETEXT +key usage 0x000c0061 CAPTIONS + +key usage 0x000c01BD INFO +key usage 0x000c0037 PERIOD + +key usage 0x000c0069 PROG_RED +key usage 0x000c006A PROG_GREEN +key usage 0x000c006C PROG_YELLOW +key usage 0x000c006B PROG_BLUE +key usage 0x000c00B4 MEDIA_SKIP_BACKWARD +key usage 0x000c00CD MEDIA_PLAY_PAUSE +key usage 0x000c00B2 MEDIA_RECORD +key usage 0x000c00B3 MEDIA_SKIP_FORWARD + +key usage 0x000c022A BOOKMARK +key usage 0x000c01A2 ALL_APPS +key usage 0x000c019C PROFILE_SWITCH + +key usage 0x000c0096 SETTINGS +key usage 0x000c009F NOTIFICATION + +key usage 0x000c008D GUIDE +key usage 0x000c0089 TV + +key usage 0x000c0187 FEATURED_APP_1 WAKE #FreeTv + +key usage 0x000c009C CHANNEL_UP +key usage 0x000c009D CHANNEL_DOWN + +key usage 0x000c0077 BUTTON_3 WAKE #YouTube +key usage 0x000c0078 BUTTON_4 WAKE #Netflix +key usage 0x000c0079 BUTTON_6 WAKE +key usage 0x000c007A BUTTON_7 WAKE
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 592f9a57884c..80afb16d5832 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -382,9 +382,13 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { if (splitAttributes == null) { return TaskFragmentAnimationParams.DEFAULT; } - return new TaskFragmentAnimationParams.Builder() - // TODO(b/263047900): Update extensions API. - // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) - .build(); + final AnimationBackground animationBackground = splitAttributes.getAnimationBackground(); + if (animationBackground instanceof AnimationBackground.ColorBackground colorBackground) { + return new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(colorBackground.getColor()) + .build(); + } else { + return TaskFragmentAnimationParams.DEFAULT; + } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 6f356fa35d41..8b7fd108f031 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -893,8 +893,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return new SplitAttributes.Builder() .setSplitType(splitTypeToUpdate) .setLayoutDirection(splitAttributes.getLayoutDirection()) - // TODO(b/263047900): Update extensions API. - // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + .setAnimationBackground(splitAttributes.getAnimationBackground()) .build(); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index 60beb0b7f0a4..f471af052bf2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -25,6 +25,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.AnimationBackground; import androidx.window.extensions.embedding.SplitAttributes; import org.junit.Before; @@ -70,7 +71,7 @@ public class WindowExtensionsTest { .isEqualTo(SplitAttributes.LayoutDirection.LOCALE); assertThat(splitAttributes.getSplitType()) .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f)); - // TODO(b/263047900): Update extensions API. - // assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0); + assertThat(splitAttributes.getAnimationBackground()) + .isEqualTo(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT); } } diff --git a/libs/WindowManager/Shell/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/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/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/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/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/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 0eabe66e9a69..838630fcccb9 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -943,6 +943,10 @@ public final class MediaRoute2Info implements Parcelable { .append(getId()) .append(", name=") .append(getName()) + .append(", type=") + .append(getDeviceTypeString(getType())) + .append(", isSystem=") + .append(isSystemRoute()) .append(", features=") .append(getFeatures()) .append(", iconUri=") diff --git a/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/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index a03fa9b39bfc..7443e4ccf79e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -84,6 +84,7 @@ <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/> <uses-permission android:name="android.permission.LOCATION_HARDWARE" /> <uses-permission android:name="android.permission.NETWORK_FACTORY" /> + <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" /> <!-- Physical hardware --> <uses-permission android:name="android.permission.MANAGE_USB" /> <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" /> diff --git a/packages/SystemUI/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 fb023da399f0..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 @@ -26,6 +26,7 @@ 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 @@ -44,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 @@ -51,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 @@ -71,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 @@ -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,7 +213,7 @@ private fun BoxScope.CommunalHubLazyGrid( if (viewModel.isEditMode && dragDropState != null) { DraggableItem( dragDropState = dragDropState, - enabled = true, + enabled = list[index] is CommunalContentModel.Widget, index = index, size = size ) { _ -> @@ -216,6 +222,7 @@ private fun BoxScope.CommunalHubLazyGrid( model = list[index], viewModel = viewModel, size = size, + onOpenWidgetPicker = onOpenWidgetPicker, ) } } else { @@ -256,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 = filledButtonColors(), - contentPadding = buttonContentPadding + contentPadding = Dimensions.ButtonPadding ) { Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor)) Spacer(spacerModifier) @@ -285,7 +287,7 @@ private fun Toolbar( disabledContainerColor = colors.primary, disabledContentColor = colors.onPrimary, ), - contentPadding = buttonContentPadding, + contentPadding = Dimensions.ButtonPadding, modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } ) { RemoveButtonContent(spacerModifier) @@ -297,7 +299,7 @@ private fun Toolbar( onClick = {}, colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary), border = BorderStroke(width = 1.0.dp, color = colors.primary), - contentPadding = buttonContentPadding, + contentPadding = Dimensions.ButtonPadding, modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } ) { RemoveButtonContent(spacerModifier) @@ -307,7 +309,7 @@ private fun Toolbar( Button( onClick = onEditDone, colors = filledButtonColors(), - contentPadding = buttonContentPadding + contentPadding = Dimensions.ButtonPadding ) { Text( text = stringResource(R.string.hub_mode_editing_exit_button_text), @@ -340,10 +342,15 @@ private fun CommunalContent( viewModel: BaseCommunalViewModel, size: SizeF, modifier: Modifier = Modifier, + onOpenWidgetPicker: (() -> Unit)? = null, ) { when (model) { 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) @@ -361,6 +368,115 @@ 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, @@ -513,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/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/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index 449ee6f414dd..4079f1241f31 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -19,14 +19,14 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo -import android.content.BroadcastReceiver import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ACTION_USER_UNLOCKED import android.os.UserHandle import android.os.UserManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem @@ -38,15 +38,12 @@ import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -55,8 +52,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -70,8 +67,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: AppWidgetHost - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var userManager: UserManager @Mock private lateinit var userHandle: UserHandle @@ -125,10 +120,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { communalEnabled(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - verify(communalWidgetDao, Mockito.never()).getWidgets() + verify(communalWidgetDao, never()).getWidgets() } @Test @@ -136,10 +131,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - verify(communalWidgetDao, Mockito.never()).getWidgets() + verify(communalWidgetDao, never()).getWidgets() } @Test @@ -147,8 +142,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - val communalWidgets = collectLastValue(repository.communalWidgets) - communalWidgets() + val communalWidgets by collectLastValue(repository.communalWidgets) runCurrent() val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) @@ -158,11 +152,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { userUnlocked(true) installedProviders(listOf(stopwatchProviderInfo)) - broadcastReceiverUpdate() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) runCurrent() verify(communalWidgetDao).getWidgets() - assertThat(communalWidgets()) + assertThat(communalWidgets) .containsExactly( CommunalWidgetContentModel( appWidgetId = communalWidgetItemEntry.widgetId, @@ -182,9 +179,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) .thenReturn(id) - repository.addWidget(provider, priority) + repository.addWidget(provider, priority) { true } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -192,75 +190,117 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun deleteWidget_removeWidgetId_andDeleteFromDb() = + fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { userUnlocked(true) val repository = initCommunalWidgetRepository() runCurrent() + val provider = ComponentName("pkg_name", "cls_name") val id = 1 - repository.deleteWidget(id) + val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) + whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) + repository.addWidget(provider, priority) { false } runCurrent() - verify(communalWidgetDao).deleteWidgetById(id) + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao, never()).addWidget(id, provider, priority) verify(appWidgetHost).deleteAppWidgetId(id) } @Test - fun reorderWidgets_queryDb() = + fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { userUnlocked(true) val repository = initCommunalWidgetRepository() runCurrent() - val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) - repository.updateWidgetOrder(widgetIdToPriorityMap) + val provider = ComponentName("pkg_name", "cls_name") + val id = 1 + val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(true) + whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) + repository.addWidget(provider, priority) { throw IllegalStateException("some error") } runCurrent() - verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao, never()).addWidget(id, provider, priority) + verify(appWidgetHost).deleteAppWidgetId(id) } @Test - fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = + fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - communalEnabled(false) + userUnlocked(true) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verifyBroadcastReceiverNeverRegistered() + runCurrent() + + val provider = ComponentName("pkg_name", "cls_name") + val id = 1 + val priority = 1 + whenever(communalWidgetHost.requiresConfiguration(id)).thenReturn(false) + whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) + .thenReturn(id) + var configured = false + repository.addWidget(provider, priority) { + configured = true + true + } + runCurrent() + + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao).addWidget(id, provider, priority) + assertThat(configured).isFalse() } @Test - fun broadcastReceiver_featureEnabledAndUserUnlocked_doNotRegisterBroadcastReceiver() = + fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { userUnlocked(true) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verifyBroadcastReceiverNeverRegistered() + runCurrent() + + val id = 1 + repository.deleteWidget(id) + runCurrent() + + verify(communalWidgetDao).deleteWidgetById(id) + verify(appWidgetHost).deleteAppWidgetId(id) } @Test - fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() = + fun reorderWidgets_queryDb() = testScope.runTest { - userUnlocked(false) + userUnlocked(true) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verifyBroadcastReceiverRegistered() + runCurrent() + + val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) + repository.updateWidgetOrder(widgetIdToPriorityMap) + runCurrent() + + verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } @Test - fun broadcastReceiver_whenFlowFinishes_unregisterBroadcastReceiver() = + fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = testScope.runTest { - userUnlocked(false) + communalEnabled(false) val repository = initCommunalWidgetRepository() - - val job = launch { repository.communalWidgets.collect() } + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - val receiver = broadcastReceiverUpdate() + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0) + } - job.cancel() + @Test + fun broadcastReceiver_featureEnabledAndUserLocked_registerBroadcastReceiver() = + testScope.runTest { + userUnlocked(false) + val repository = initCommunalWidgetRepository() + repository.communalWidgets.launchIn(backgroundScope) runCurrent() - - verify(broadcastDispatcher).unregisterReceiver(receiver) + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1) } @Test @@ -268,12 +308,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() - verify(appWidgetHost, Mockito.never()).startListening() + repository.communalWidgets.launchIn(backgroundScope) + runCurrent() + verify(appWidgetHost, never()).startListening() userUnlocked(true) - broadcastReceiverUpdate() - collectLastValue(repository.communalWidgets)() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) + runCurrent() verify(appWidgetHost).startListening() } @@ -283,18 +327,25 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { userUnlocked(false) val repository = initCommunalWidgetRepository() - collectLastValue(repository.communalWidgets)() + repository.communalWidgets.launchIn(backgroundScope) + runCurrent() userUnlocked(true) - broadcastReceiverUpdate() - collectLastValue(repository.communalWidgets)() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) + runCurrent() verify(appWidgetHost).startListening() - verify(appWidgetHost, Mockito.never()).stopListening() + verify(appWidgetHost, never()).stopListening() userUnlocked(false) - broadcastReceiverUpdate() - collectLastValue(repository.communalWidgets)() + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(ACTION_USER_UNLOCKED) + ) + runCurrent() verify(appWidgetHost).stopListening() } @@ -305,7 +356,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { appWidgetHost, testScope.backgroundScope, testDispatcher, - broadcastDispatcher, + fakeBroadcastDispatcher, communalRepository, communalWidgetHost, communalWidgetDao, @@ -315,45 +366,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) } - private fun verifyBroadcastReceiverRegistered() { - verify(broadcastDispatcher) - .registerReceiver( - any(), - any(), - nullable(), - nullable(), - anyInt(), - nullable(), - ) - } - - private fun verifyBroadcastReceiverNeverRegistered() { - verify(broadcastDispatcher, Mockito.never()) - .registerReceiver( - any(), - any(), - nullable(), - nullable(), - anyInt(), - nullable(), - ) - } - - private fun broadcastReceiverUpdate(): BroadcastReceiver { - val broadcastReceiverCaptor = kotlinArgumentCaptor<BroadcastReceiver>() - verify(broadcastDispatcher) - .registerReceiver( - broadcastReceiverCaptor.capture(), - any(), - nullable(), - nullable(), - anyInt(), - nullable(), - ) - broadcastReceiverCaptor.value.onReceive(null, null) - return broadcastReceiverCaptor.value - } - private fun communalEnabled(enabled: Boolean) { communalRepository.setIsCommunalEnabled(enabled) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index a4940c3562cb..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 @@ -327,6 +327,32 @@ class CommunalInteractorTest : SysuiTestCase() { } @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 fun listensToSceneChange() = testScope.runTest { var desiredScene = collectLastValue(underTest.desiredScene) 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 9b7688a2a832..c638e1ea89ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -16,7 +16,11 @@ package com.android.systemui.communal.view.viewmodel +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK import android.app.smartspace.SmartspaceTarget +import android.appwidget.AppWidgetHost +import android.content.ComponentName import android.os.PowerManager import android.provider.Settings import android.widget.RemoteViews @@ -42,6 +46,8 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -50,12 +56,14 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var appWidgetHost: AppWidgetHost private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -73,7 +81,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - val withDeps = CommunalInteractorFactory.create() + val withDeps = CommunalInteractorFactory.create(testScope) keyguardRepository = withDeps.keyguardRepository communalRepository = withDeps.communalRepository tutorialRepository = withDeps.tutorialRepository @@ -84,6 +92,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { underTest = CommunalEditModeViewModel( withDeps.communalInteractor, + appWidgetHost, Provider { shadeViewController }, powerManager, mediaHost, @@ -91,7 +100,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test - fun communalContent_onlyWidgetsAreShownInEditMode() = + fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) @@ -123,12 +132,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { val communalContent by collectLastValue(underTest.communalContent) - // Only Widgets are shown. - assertThat(communalContent?.size).isEqualTo(2) + // Only Widgets and CTA tile are shown. + assertThat(communalContent?.size).isEqualTo(3) assertThat(communalContent?.get(0)) .isInstanceOf(CommunalContentModel.Widget::class.java) assertThat(communalContent?.get(1)) .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(2)) + .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java) } @Test @@ -143,4 +154,53 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { ) .isEqualTo(false) } + + @Test + fun addingWidgetTriggersConfiguration() = + testScope.runTest { + val provider = ComponentName("pkg.test", "testWidget") + val widgetToConfigure by collectLastValue(underTest.widgetsToConfigure) + assertThat(widgetToConfigure).isNull() + underTest.onAddWidget(componentName = provider, priority = 0) + assertThat(widgetToConfigure).isEqualTo(1) + } + + @Test + fun settingResultOkAddsWidget() = + testScope.runTest { + val provider = ComponentName("pkg.test", "testWidget") + val widgetAdded by collectLastValue(widgetRepository.widgetAdded) + assertThat(widgetAdded).isNull() + underTest.onAddWidget(componentName = provider, priority = 0) + assertThat(widgetAdded).isNull() + underTest.setConfigurationResult(RESULT_OK) + assertThat(widgetAdded).isEqualTo(1) + } + + @Test + fun settingResultCancelledDoesNotAddWidget() = + testScope.runTest { + val provider = ComponentName("pkg.test", "testWidget") + val widgetAdded by collectLastValue(widgetRepository.widgetAdded) + assertThat(widgetAdded).isNull() + underTest.onAddWidget(componentName = provider, priority = 0) + assertThat(widgetAdded).isNull() + underTest.setConfigurationResult(RESULT_CANCELED) + assertThat(widgetAdded).isNull() + } + + @Test(expected = IllegalStateException::class) + fun settingResultBeforeWidgetAddedThrowsException() { + underTest.setConfigurationResult(RESULT_OK) + } + + @Test(expected = IllegalStateException::class) + fun addingWidgetWhileConfigurationActiveFails() = + testScope.runTest { + val providerOne = ComponentName("pkg.test", "testWidget") + underTest.onAddWidget(componentName = providerOne, priority = 0) + runCurrent() + val providerTwo = ComponentName("pkg.test", "testWidget2") + underTest.onAddWidget(componentName = providerTwo, priority = 0) + } } 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 6240f6a2e937..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) @@ -142,10 +142,13 @@ class CommunalViewModelTest : SysuiTestCase() { // Media playing. mediaRepository.mediaActive() + // CTA Tile not dismissed. + communalRepository.setCtaTileInViewModeVisibility(true) + val communalContent by collectLastValue(underTest.communalContent) - // Order is smart space, then UMO, 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/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 7c3dc972cfd0..5b88ebe69bfe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -32,7 +32,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.statusbar.notification.data.repository.fakeNotificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.testKosmos @@ -56,8 +56,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val screenOffAnimationController = kosmos.screenOffAnimationController private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository - private val fakeNotificationsKeyguardViewStateRepository = - kosmos.fakeNotificationsKeyguardViewStateRepository + private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor private val dozeParameters = kosmos.dozeParameters private val underTest = kosmos.keyguardRootViewModel @@ -118,7 +117,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(true) + notificationsKeyguardInteractor.setPulseExpanding(true) deviceEntryRepository.setBypassEnabled(false) runCurrent() @@ -130,9 +129,9 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(true) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -144,10 +143,10 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(false) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -159,11 +158,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(true) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -175,11 +174,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.value).isTrue() @@ -191,11 +190,11 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - fakeNotificationsKeyguardViewStateRepository.setPulseExpanding(false) + notificationsKeyguardInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) - fakeNotificationsKeyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() assertThat(isVisible?.isAnimating).isEqualTo(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 75d1869adc7c..a9ee4055d1a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -68,6 +68,8 @@ class FakeMobileIconsInteractor( override val isSingleCarrier = MutableStateFlow(true) + override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList()) + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping @@ -80,8 +82,12 @@ class FakeMobileIconsInteractor( override val isForceHidden = MutableStateFlow(false) /** Always returns a new fake interactor */ - override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { - return FakeMobileIconInteractor(tableLogBuffer).also { interactorCache[subId] = it } + override fun getMobileConnectionInteractorForSubId(subId: Int): FakeMobileIconInteractor { + return FakeMobileIconInteractor(tableLogBuffer).also { + interactorCache[subId] = it + // Also update the icons + icons.value = interactorCache.values.toList() + } } /** diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 0537f17b3594..9063a02ee885 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -46,8 +46,8 @@ java_library { static_libs: [ "androidx.annotation_annotation", "androidx-constraintlayout_constraintlayout", + "PlatformAnimationLib", "PluginCoreLib", - "SystemUIAnimationLib", "SystemUICommon", "SystemUILogLib", "androidx.annotation_annotation", diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index 7ab44e70e6fe..73874a08b0bd 100644 --- a/packages/SystemUI/res/layout/qs_footer_impl.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -44,6 +44,8 @@ android:ellipsize="marquee" android:focusable="true" android:gravity="center_vertical" + android:textDirection="locale" + android:textAlignment="viewStart" android:singleLine="true" android:textAppearance="@style/TextAppearance.QS.Status.Build" android:visibility="gone" /> diff --git a/packages/SystemUI/res/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/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/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 1f4be4060223..553b3ebc0813 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -56,6 +56,9 @@ interface CommunalRepository { /** Exposes the transition state of the communal [SceneTransitionLayout]. */ val transitionState: StateFlow<ObservableCommunalTransitionState> + /** Whether the CTA tile is visible in the hub under view mode. */ + val isCtaTileInViewModeVisible: Flow<Boolean> + /** Updates the requested scene. */ fun setDesiredScene(desiredScene: CommunalSceneKey) @@ -65,6 +68,9 @@ interface CommunalRepository { * Note that you must call is with `null` when the UI is done or risk a memory leak. */ fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?) + + /** Updates whether to display the CTA tile in the hub under view mode. */ + fun setCtaTileInViewModeVisibility(isVisible: Boolean) } @OptIn(ExperimentalCoroutinesApi::class) @@ -96,6 +102,16 @@ constructor( initialValue = defaultTransitionState, ) + // TODO(b/313462210) - persist the value in local storage, so the tile won't show up again + // once dismissed. + private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) + override val isCtaTileInViewModeVisible: Flow<Boolean> = + _isCtaTileInViewModeVisible.asStateFlow() + + override fun setCtaTileInViewModeVisibility(isVisible: Boolean) { + _isCtaTileInViewModeVisible.value = isVisible + } + override fun setDesiredScene(desiredScene: CommunalSceneKey) { _desiredScene.value = desiredScene } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index cab8adfc0bd9..e6816e954b5d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -18,14 +18,12 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager -import android.content.BroadcastReceiver import android.content.ComponentName -import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.UserManager +import androidx.annotation.WorkerThread import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem @@ -40,17 +38,21 @@ import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.settings.UserTracker import java.util.Optional import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** Encapsulates the state of widgets for communal mode. */ interface CommunalWidgetRepository { @@ -58,7 +60,11 @@ interface CommunalWidgetRepository { val communalWidgets: Flow<List<CommunalWidgetContentModel>> /** Add a widget at the specified position in the app widget service and the database. */ - fun addWidget(provider: ComponentName, priority: Int) {} + fun addWidget( + provider: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) {} /** Delete a widget by id from app widget service and the database. */ fun deleteWidget(widgetId: Int) {} @@ -97,37 +103,22 @@ constructor( // Whether the [AppWidgetHost] is listening for updates. private var isHostListening = false - private val isUserUnlocked: Flow<Boolean> = - callbackFlow { - if (!communalRepository.isCommunalEnabled) { - awaitClose() - } + private suspend fun isUserUnlockingOrUnlocked(): Boolean = + withContext(bgDispatcher) { userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) } - fun isUserUnlockingOrUnlocked(): Boolean { - return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) - } - - fun send() { - trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG) - } - - if (isUserUnlockingOrUnlocked()) { - send() - awaitClose() + private val isUserUnlocked: Flow<Boolean> = + flowOf(communalRepository.isCommunalEnabled) + .flatMapLatest { enabled -> + if (enabled) { + broadcastDispatcher + .broadcastFlow( + filter = IntentFilter(Intent.ACTION_USER_UNLOCKED), + user = userTracker.userHandle + ) + .onStart { emit(Unit) } + .mapLatest { isUserUnlockingOrUnlocked() } } else { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - send() - } - } - - broadcastDispatcher.registerReceiver( - receiver, - IntentFilter(Intent.ACTION_USER_UNLOCKED), - ) - - awaitClose { broadcastDispatcher.unregisterReceiver(receiver) } + emptyFlow() } } .distinctUntilChanged() @@ -148,18 +139,52 @@ constructor( if (!isHostActive || !appWidgetManager.isPresent) { return@flatMapLatest flowOf(emptyList()) } - communalWidgetDao.getWidgets().map { it.map(::mapToContentModel) } + communalWidgetDao + .getWidgets() + .map { it.map(::mapToContentModel) } + // As this reads from a database and triggers IPCs to AppWidgetManager, + // it should be executed in the background. + .flowOn(bgDispatcher) } - override fun addWidget(provider: ComponentName, priority: Int) { + override fun addWidget( + provider: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) { applicationScope.launch(bgDispatcher) { val id = communalWidgetHost.allocateIdAndBindWidget(provider) - id?.let { - communalWidgetDao.addWidget( - widgetId = it, - provider = provider, - priority = priority, - ) + if (id != null) { + val configured = + if (communalWidgetHost.requiresConfiguration(id)) { + logger.i("Widget ${provider.flattenToString()} requires configuration.") + try { + configureWidget.invoke(id) + } catch (ex: Exception) { + // Cleanup the app widget id if an error happens during configuration. + logger.e("Error during widget configuration, cleaning up id $id", ex) + if (ex is CancellationException) { + appWidgetHost.deleteAppWidgetId(id) + // Re-throw cancellation to ensure the parent coroutine also gets + // cancelled. + throw ex + } else { + false + } + } + } else { + logger.i("Skipping configuration for ${provider.flattenToString()}") + true + } + if (configured) { + communalWidgetDao.addWidget( + widgetId = id, + provider = provider, + priority = priority, + ) + } else { + appWidgetHost.deleteAppWidgetId(id) + } } logger.i("Added widget ${provider.flattenToString()} at position $priority.") } @@ -182,6 +207,7 @@ constructor( } } + @WorkerThread private fun mapToContentModel( entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> ): CommunalWidgetContentModel { diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index c0fdbf7627bd..24d4c6c4c397 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -96,9 +96,20 @@ constructor( editWidgetsActivityStarter.startActivity() } - /** Add a widget at the specified position. */ - fun addWidget(componentName: ComponentName, priority: Int) = - widgetRepository.addWidget(componentName, priority) + /** Dismiss the CTA tile from the hub in view mode. */ + fun dismissCtaTile() = communalRepository.setCtaTileInViewModeVisibility(isVisible = false) + + /** + * Add a widget at the specified position. + * + * @param configureWidget The callback to trigger if widget configuration is needed. Should + * return whether configuration was successful. + */ + fun addWidget( + componentName: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) = widgetRepository.addWidget(componentName, priority, configureWidget) /** Delete a widget by id. */ fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) @@ -136,6 +147,12 @@ constructor( } } + /** CTA tile to be displayed in the glanceable hub (view mode). */ + val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> = + communalRepository.isCtaTileInViewModeVisible.map { visible -> + if (visible) listOf(CommunalContentModel.CtaTileInViewMode()) else emptyList() + } + /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ val tutorialContent: List<CommunalContentModel.Tutorial> = listOf( 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 e6cee38aa57f..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 @@ -58,6 +58,20 @@ sealed interface CommunalContentModel { 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 var size: CommunalContentSize, @@ -83,6 +97,9 @@ sealed interface CommunalContentModel { 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" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt index 155de323d3a6..41f9cb4c98ed 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -18,6 +18,8 @@ package com.android.systemui.communal.shared import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL +import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger @@ -63,4 +65,23 @@ constructor( } return false } + + /** + * Returns whether a particular widget requires configuration when it is first added. + * + * Must be called after the widget id has been bound. + */ + fun requiresConfiguration(widgetId: Int): Boolean { + if (appWidgetManager.isPresent) { + val widgetInfo = appWidgetManager.get().getAppWidgetInfo(widgetId) + val featureFlags: Int = widgetInfo.widgetFeatures + // A widget's configuration is optional only if it's configuration is marked as optional + // AND it can be reconfigured later. + val configurationOptional = + (featureFlags and WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0 && + featureFlags and WIDGET_FEATURE_RECONFIGURABLE != 0) + return widgetInfo.configure != null && !configurationOptional + } + return false + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index c34a8df1bed7..97e530ace9a7 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 @@ -58,8 +58,16 @@ abstract class BaseCommunalViewModel( /** * Called when a widget is added via drag and drop from the widget picker into the communal hub. */ - fun onAddWidget(componentName: ComponentName, priority: Int) { - communalInteractor.addWidget(componentName, priority) + open fun onAddWidget(componentName: ComponentName, priority: Int) { + communalInteractor.addWidget(componentName, priority, ::configureWidget) + } + + /** + * Called when a widget needs to be configured, with the id of the widget. The return value + * should represent whether configuring the widget was successful. + */ + protected open suspend fun configureWidget(widgetId: Int): Boolean { + return true } // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block @@ -103,6 +111,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..a03e6c1aee97 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -16,6 +16,13 @@ package com.android.systemui.communal.ui.viewmodel +import android.app.Activity +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_OK +import android.app.ActivityOptions +import android.appwidget.AppWidgetHost +import android.content.ActivityNotFoundException +import android.content.ComponentName import android.os.PowerManager import android.widget.RemoteViews import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -24,10 +31,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import com.android.systemui.shade.ShadeViewController +import com.android.systemui.util.nullableAtomicReference import javax.inject.Inject import javax.inject.Named import javax.inject.Provider +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.map /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -35,16 +46,34 @@ class CommunalEditModeViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, + private val appWidgetHost: AppWidgetHost, shadeViewController: Provider<ShadeViewController>, powerManager: PowerManager, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, ) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) { + private companion object { + private const val KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle" + private const val SPLASH_SCREEN_STYLE_EMPTY = 0 + } + + private val _widgetsToConfigure = MutableSharedFlow<Int>() + + /** + * Flow emitting ids of widgets which need to be configured. The consumer of this flow should + * trigger [startConfigurationActivity] to initiate configuration. + */ + val widgetsToConfigure: Flow<Int> = _widgetsToConfigure + + private var pendingConfiguration: CompletableDeferred<Int>? by nullableAtomicReference() + override val isEditMode = true - // Only widgets are editable. + // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent + communalInteractor.widgetContent.map { widgets -> + widgets + listOf(CommunalContentModel.CtaTileInEditMode()) + } override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) @@ -55,4 +84,55 @@ constructor( // Ignore all interactions in edit mode. return RemoteViews.InteractionHandler { _, _, _ -> false } } + + override fun onAddWidget(componentName: ComponentName, priority: Int) { + if (pendingConfiguration != null) { + throw IllegalStateException( + "Cannot add $componentName widget while widget configuration is pending" + ) + } + super.onAddWidget(componentName, priority) + } + + fun startConfigurationActivity(activity: Activity, widgetId: Int, requestCode: Int) { + val options = + ActivityOptions.makeBasic().apply { + setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + } + val bundle = options.toBundle() + bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY) + try { + appWidgetHost.startAppWidgetConfigureActivityForResult( + activity, + widgetId, + 0, + // Use the widget id as the request code. + requestCode, + bundle + ) + } catch (e: ActivityNotFoundException) { + setConfigurationResult(RESULT_CANCELED) + } + } + + override suspend fun configureWidget(widgetId: Int): Boolean { + if (pendingConfiguration != null) { + throw IllegalStateException( + "Attempting to configure $widgetId while another configuration is already active" + ) + } + pendingConfiguration = CompletableDeferred() + _widgetsToConfigure.emit(widgetId) + val resultCode = pendingConfiguration?.await() ?: RESULT_CANCELED + pendingConfiguration = null + return resultCode == RESULT_OK + } + + /** Sets the result of widget configuration. */ + fun setConfigurationResult(resultCode: Int) { + pendingConfiguration?.complete(resultCode) + ?: throw IllegalStateException("No widget pending configuration") + } } 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 d8683d67f32b..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 @@ -56,12 +56,16 @@ constructor( combine( communalInteractor.ongoingContent, communalInteractor.widgetContent, - ) { ongoing, widgets -> - ongoing + 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/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 0f94a92dd7ce..bfc6f2b14acd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -27,23 +27,26 @@ import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import com.android.systemui.communal.domain.interactor.CommunalInteractor +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent import javax.inject.Inject +import kotlinx.coroutines.launch /** An Activity for editing the widgets that appear in hub mode. */ class EditWidgetsActivity @Inject constructor( private val communalViewModel: CommunalEditModeViewModel, - private val communalInteractor: CommunalInteractor, private var windowManagerService: IWindowManager? = null, ) : ComponentActivity() { companion object { private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" private const val EXTRA_FILTER_STRATEGY = "filter_strategy" private const val FILTER_STRATEGY_GLANCEABLE_HUB = 1 + private const val REQUEST_CODE_CONFIGURE_WIDGET = 1 private const val TAG = "EditWidgetsActivity" } @@ -63,7 +66,7 @@ constructor( Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java ) - ?.let { communalInteractor.addWidget(it, 0) } + ?.let { communalViewModel.onAddWidget(it, 0) } ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } } } @@ -84,14 +87,26 @@ constructor( windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // Start the configuration activity when new widgets are added. + communalViewModel.widgetsToConfigure.collect { widgetId -> + communalViewModel.startConfigurationActivity( + activity = this@EditWidgetsActivity, + widgetId = widgetId, + requestCode = REQUEST_CODE_CONFIGURE_WIDGET + ) + } + } + } + setCommunalEditWidgetActivityContent( activity = this, viewModel = communalViewModel, onOpenWidgetPicker = { - val localPackageManager: PackageManager = getPackageManager() val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } - localPackageManager + packageManager .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ?.activityInfo ?.packageName @@ -122,4 +137,11 @@ constructor( } ) } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_CONFIGURE_WIDGET) { + communalViewModel.setConfigurationResult(resultCode) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 8b992fcae67e..b2d70523c282 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -91,6 +91,7 @@ import android.telecom.TelecomManager; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.telephony.satellite.SatelliteManager; import android.view.Choreographer; import android.view.CrossWindowBlurListeners; import android.view.IWindowManager; @@ -712,4 +713,10 @@ public class FrameworkServicesModule { ServiceManager.getService(Context.URI_GRANTS_SERVICE) ); } + + @Provides + @Singleton + static Optional<SatelliteManager> provideSatelliteManager(Context context) { + return Optional.ofNullable(context.getSystemService(SatelliteManager.class)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index 615b503b9fae..3bc4f342c566 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -32,6 +32,7 @@ import android.widget.FrameLayout import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.ScreenDecorationsLogger @@ -41,19 +42,20 @@ import javax.inject.Inject @SysUISingleton class FaceScanningProviderFactory @Inject constructor( - private val authController: AuthController, - private val context: Context, - private val statusBarStateController: StatusBarStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - @Main private val mainExecutor: Executor, - private val logger: ScreenDecorationsLogger, + private val authController: AuthController, + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + @Main private val mainExecutor: Executor, + private val logger: ScreenDecorationsLogger, + private val facePropertyRepository: FacePropertyRepository, ) : DecorProviderFactory() { private val display = context.display private val displayInfo = DisplayInfo() override val hasProviders: Boolean get() { - if (authController.faceSensorLocation == null) { + if (facePropertyRepository.sensorLocation.value == null) { return false } @@ -86,6 +88,7 @@ class FaceScanningProviderFactory @Inject constructor( keyguardUpdateMonitor, mainExecutor, logger, + facePropertyRepository, ) ) } @@ -104,12 +107,13 @@ class FaceScanningProviderFactory @Inject constructor( } class FaceScanningOverlayProviderImpl( - override val alignedBound: Int, - private val authController: AuthController, - private val statusBarStateController: StatusBarStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val mainExecutor: Executor, - private val logger: ScreenDecorationsLogger, + override val alignedBound: Int, + private val authController: AuthController, + private val statusBarStateController: StatusBarStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mainExecutor: Executor, + private val logger: ScreenDecorationsLogger, + private val facePropertyRepository: FacePropertyRepository, ) : BoundDecorProvider() { override val viewId: Int = com.android.systemui.res.R.id.face_scanning_anim @@ -162,8 +166,9 @@ class FaceScanningOverlayProviderImpl( layoutParams.let { lp -> lp.width = ViewGroup.LayoutParams.MATCH_PARENT lp.height = ViewGroup.LayoutParams.MATCH_PARENT - logger.faceSensorLocation(authController.faceSensorLocation) - authController.faceSensorLocation?.y?.let { faceAuthSensorHeight -> + logger.faceSensorLocation(facePropertyRepository.sensorLocation.value) + facePropertyRepository.sensorLocation.value?.y?.let { + faceAuthSensorHeight -> val faceScanningHeight = (faceAuthSensorHeight * 2) when (rotation) { Surface.ROTATION_0, Surface.ROTATION_180 -> diff --git a/packages/SystemUI/src/com/android/systemui/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/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/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/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 0c67279c1660..3f2c818399d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -31,6 +31,7 @@ import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.StackStateAnimator @@ -58,6 +59,7 @@ constructor( private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController, private val logger: NotificationWakeUpCoordinatorLogger, + private val notifsKeyguardInteractor: NotificationsKeyguardInteractor, ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, @@ -144,6 +146,7 @@ constructor( for (listener in wakeUpListeners) { listener.onFullyHiddenChanged(value) } + notifsKeyguardInteractor.setNotificationsFullyHidden(value) } } @@ -216,6 +219,7 @@ constructor( for (listener in wakeUpListeners) { listener.onPulseExpandingChanged(pulseExpanding) } + notifsKeyguardInteractor.setPulseExpanding(pulseExpanding) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt index 5435fb5449cd..2cac0002f013 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt @@ -15,8 +15,6 @@ */ package com.android.systemui.statusbar.notification.data -import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardStateRepositoryModule import dagger.Module -@Module(includes = [NotificationsKeyguardStateRepositoryModule::class]) -interface NotificationDataLayerModule +@Module(includes = []) interface NotificationDataLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt index 2cc1403a80a5..bd6ea30c44e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt @@ -15,59 +15,16 @@ */ package com.android.systemui.statusbar.notification.data.repository -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator -import dagger.Binds -import dagger.Module import javax.inject.Inject -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow /** View-states pertaining to notifications on the keyguard. */ -interface NotificationsKeyguardViewStateRepository { +@SysUISingleton +class NotificationsKeyguardViewStateRepository @Inject constructor() { /** Are notifications fully hidden from view? */ - val areNotificationsFullyHidden: Flow<Boolean> + val areNotificationsFullyHidden = MutableStateFlow(false) /** Is a pulse expansion occurring? */ - val isPulseExpanding: Flow<Boolean> -} - -@Module -interface NotificationsKeyguardStateRepositoryModule { - @Binds - fun bindImpl( - impl: NotificationsKeyguardViewStateRepositoryImpl - ): NotificationsKeyguardViewStateRepository -} - -@SysUISingleton -class NotificationsKeyguardViewStateRepositoryImpl -@Inject -constructor( - wakeUpCoordinator: NotificationWakeUpCoordinator, -) : NotificationsKeyguardViewStateRepository { - override val areNotificationsFullyHidden: Flow<Boolean> = conflatedCallbackFlow { - val listener = - object : NotificationWakeUpCoordinator.WakeUpListener { - override fun onFullyHiddenChanged(isFullyHidden: Boolean) { - trySend(isFullyHidden) - } - } - trySend(wakeUpCoordinator.notificationsFullyHidden) - wakeUpCoordinator.addListener(listener) - awaitClose { wakeUpCoordinator.removeListener(listener) } - } - - override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow { - val listener = - object : NotificationWakeUpCoordinator.WakeUpListener { - override fun onPulseExpandingChanged(isPulseExpanding: Boolean) { - trySend(isPulseExpanding) - } - } - trySend(wakeUpCoordinator.isPulseExpanding()) - wakeUpCoordinator.addListener(listener) - awaitClose { wakeUpCoordinator.removeListener(listener) } - } + val isPulseExpanding = MutableStateFlow(false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt index 73341dbc4999..a6361cbc9f9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt @@ -15,24 +15,29 @@ */ package com.android.systemui.statusbar.notification.domain.interactor -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn /** Domain logic pertaining to notifications on the keyguard. */ class NotificationsKeyguardInteractor @Inject constructor( - repository: NotificationsKeyguardViewStateRepository, - @Background backgroundDispatcher: CoroutineDispatcher, + private val repository: NotificationsKeyguardViewStateRepository, ) { /** Is a pulse expansion occurring? */ - val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding.flowOn(backgroundDispatcher) + val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding /** Are notifications fully hidden from view? */ - val areNotificationsFullyHidden: Flow<Boolean> = - repository.areNotificationsFullyHidden.flowOn(backgroundDispatcher) + val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden + + /** Updates whether notifications are fully hidden from view. */ + fun setNotificationsFullyHidden(fullyHidden: Boolean) { + repository.areNotificationsFullyHidden.value = fullyHidden + } + + /** Updates whether a pulse expansion is occurring. */ + fun setPulseExpanding(pulseExpanding: Boolean) { + repository.isPulseExpanding.value = pulseExpanding + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 9ae41951bb74..d7cbe5df419b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -14,6 +14,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_BINDABLE; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW; @@ -40,11 +41,13 @@ import com.android.systemui.statusbar.BaseStatusBarFrameLayout; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; @@ -432,6 +435,10 @@ public interface StatusBarIconController { case TYPE_MOBILE_NEW: return addNewMobileIcon(index, slot, holder.getTag()); + + case TYPE_BINDABLE: + // Safe cast, since only BindableIconHolders can set this tag on themselves + return addBindableIcon((BindableIconHolder) holder, index); } return null; @@ -446,6 +453,18 @@ public interface StatusBarIconController { return view; } + /** + * ModernStatusBarViews can be created and bound, and thus do not need to update their + * drawable by sending multiple calls to setIcon. Instead, by using a bindable + * icon view, we can simply create the icon when requested and allow the + * ViewBinder to control its visual state. + */ + protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, int index) { + ModernStatusBarView view = holder.getInitializer().createAndBind(mContext); + mGroup.addView(view, index, onCreateLayoutParams()); + return view; + } + protected StatusIconDisplayable addNewWifiIcon(int index, String slot) { ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); mGroup.addView(view, index, onCreateLayoutParams()); @@ -530,6 +549,7 @@ public interface StatusBarIconController { return; case TYPE_MOBILE_NEW: case TYPE_WIFI_NEW: + case TYPE_BINDABLE: // Nothing, the new icons update themselves return; default: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 0f4d68c68d00..4f148f112c52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -38,8 +38,11 @@ import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusIconDisplayable; +import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry; +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.tuner.TunerService; @@ -83,7 +86,8 @@ public class StatusBarIconControllerImpl implements Tunable, TunerService tunerService, DumpManager dumpManager, StatusBarIconList statusBarIconList, - StatusBarPipelineFlags statusBarPipelineFlags + StatusBarPipelineFlags statusBarPipelineFlags, + BindableIconsRegistry modernIconsRegistry ) { mStatusBarIconList = statusBarIconList; mContext = context; @@ -94,6 +98,28 @@ public class StatusBarIconControllerImpl implements Tunable, tunerService.addTunable(this, ICON_HIDE_LIST); demoModeController.addCallback(this); dumpManager.registerDumpable(getClass().getSimpleName(), this); + + addModernBindableIcons(modernIconsRegistry); + } + + /** + * BindableIcons will always produce ModernStatusBarViews, which will be initialized and bound + * upon being added to any icon group. Because their view policy does not require subsequent + * calls to setIcon(), we can simply register them all statically here and not have to build + * CoreStartables for each modern icon. + * + * @param registry a statically defined provider of the modern icons + */ + private void addModernBindableIcons(BindableIconsRegistry registry) { + List<BindableIcon> icons = registry.getBindableIcons(); + + // Initialization point for the bindable (modern) icons. These icons get their own slot + // allocated immediately, and are required to control their own display properties + for (BindableIcon i : icons) { + if (i.getShouldBindIcon()) { + addBindableIcon(i); + } + } } /** */ @@ -182,6 +208,17 @@ public class StatusBarIconControllerImpl implements Tunable, mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder)); } + void addBindableIcon(BindableIcon icon) { + StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0); + // Expected to be null + if (existingHolder == null) { + BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer()); + setIcon(icon.getSlot(), bindableIcon); + } else { + Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring"); + } + } + /** */ @Override public void setIcon(String slot, int resourceId, CharSequence contentDescription) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt index 5b55a1e73dc3..bef0b286ebf8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt @@ -21,23 +21,24 @@ import android.graphics.drawable.Icon import android.os.UserHandle import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator /** Wraps [com.android.internal.statusbar.StatusBarIcon] so we can still have a uniform list */ -class StatusBarIconHolder private constructor() { - @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW) +open class StatusBarIconHolder private constructor() { + @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW, TYPE_BINDABLE) @Retention(AnnotationRetention.SOURCE) internal annotation class IconType var icon: StatusBarIcon? = null @IconType - var type = TYPE_ICON - private set + open var type = TYPE_ICON + internal set var tag = 0 private set - var isVisible: Boolean + open var isVisible: Boolean get() = when (type) { TYPE_ICON -> icon!!.visible @@ -45,6 +46,7 @@ class StatusBarIconHolder private constructor() { // The new pipeline controls visibilities via the view model and // view binder, so // this is effectively an unused return value. + TYPE_BINDABLE, TYPE_MOBILE_NEW, TYPE_WIFI_NEW -> true else -> true @@ -55,6 +57,7 @@ class StatusBarIconHolder private constructor() { } when (type) { TYPE_ICON -> icon!!.visible = visible + TYPE_BINDABLE, TYPE_MOBILE_NEW, TYPE_WIFI_NEW -> {} } @@ -94,6 +97,9 @@ class StatusBarIconHolder private constructor() { ) const val TYPE_WIFI_NEW = 4 + /** Only applicable to [BindableIconHolder] */ + const val TYPE_BINDABLE = 5 + /** Returns a human-readable string representing the given type. */ fun getTypeString(@IconType type: Int): String { return when (type) { @@ -154,4 +160,25 @@ class StatusBarIconHolder private constructor() { return holder } } + + /** + * Subclass of StatusBarIconHolder that is responsible only for the registration of an icon into + * the [StatusBarIconList]. A bindable icon takes care of its own display, including hiding + * itself under the correct conditions. + * + * StatusBarIconController will register all available bindable icons on init (see + * [BindableIconsRepository]), and will ignore any call to setIcon for these. + * + * [initializer] a view creator that can bind the relevant view models to the created view. + */ + class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() { + override var type: Int = TYPE_BINDABLE + + /** This is unused, as bindable icons use their own view binders to control visibility */ + override var isVisible: Boolean = true + + override fun toString(): String { + return ("StatusBarIconHolder(type=BINDABLE)") + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index e1fd37f558ab..89a2fb78635b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -29,6 +29,8 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.Airplane import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigCoreStartable import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher @@ -42,6 +44,8 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder @@ -76,8 +80,16 @@ abstract class StatusBarPipelineModule { abstract fun airplaneModeViewModel(impl: AirplaneModeViewModelImpl): AirplaneModeViewModel @Binds + abstract fun bindableIconsRepository(impl: BindableIconsRegistryImpl): BindableIconsRegistry + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository + @Binds + abstract fun deviceBasedSatelliteRepository( + impl: DeviceBasedSatelliteRepositoryImpl + ): DeviceBasedSatelliteRepository + @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt new file mode 100644 index 000000000000..e3c3139f6906 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.icons.shared + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import javax.inject.Inject + +/** + * Bindable status bar icons represent icon descriptions which can be registered with + * StatusBarIconController and can also create their own bindings. A bound icon is responsible for + * its own updates via the [repeatWhenAttached] view lifecycle utility. Thus, + * StatusBarIconController can (and will) ignore any call to setIcon. + * + * In other words, these icons are bound once (at controller init) and they will control their + * visibility on their own (while their overall appearance remains at the discretion of + * StatusBarIconController, via the ModernStatusBarViewBinding interface). + */ +interface BindableIconsRegistry { + val bindableIcons: List<BindableIcon> +} + +@SysUISingleton +class BindableIconsRegistryImpl +@Inject +constructor( +/** Bindables go here */ +) : BindableIconsRegistry { + /** + * Adding the injected bindables to this list will get them registered with + * StatusBarIconController + */ + override val bindableIcons: List<BindableIcon> = listOf() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt new file mode 100644 index 000000000000..9d0d8380da45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.icons.shared.model + +/** + * A BindableIcon describes a status bar icon that can be housed in the [ModernStatusBarView] + * created by [initializer]. They can be registered statically for [BindableIconsRepositoryImpl]. + * + * Typical usage would be to create an (@SysUISingleton) adapter class that implements the + * interface. For example: + * ``` + * @SysuUISingleton + * class MyBindableIconAdapter + * @Inject constructor( + * // deps + * val viewModel: MyViewModel + * ) : BindableIcon { + * override val slot = "icon_slot_name" + * + * override val initializer = ModernStatusBarViewCreator() { + * SingleBindableStatusBarIconView.createView(context).also { iconView -> + * MyIconViewBinder.bind(iconView, viewModel) + * } + * } + * + * override fun shouldBind() = Flags.myFlag() + * } + * ``` + * + * By defining this adapter (and injecting it into the repository), we get our icon registered with + * the legacy StatusBarIconController while proxying all updates to the view binder that is created + * elsewhere. + * + * Note that the initializer block defines a closure that can pull in the viewModel dependency + * without us having to store it directly in the icon controller. + */ +interface BindableIcon { + val slot: String + val initializer: ModernStatusBarViewCreator + val shouldBindIcon: Boolean +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt new file mode 100644 index 000000000000..dbd5c1d1dede --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.icons.shared.model + +import android.content.Context +import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView + +/** + * Defined as an interface (as opposed to a typealias) to simplify calling from java. + * [ModernStatusBarViewCreator.createAndBind] should return a constructed and bound + * [ModernStatusBarView]. + */ +fun interface ModernStatusBarViewCreator { + fun createAndBind(context: Context): ModernStatusBarView +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index dad409316730..39135c70788d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -71,6 +71,12 @@ interface MobileIconsInteractor { /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow<List<SubscriptionModel>> + /** + * The current list of [MobileIconInteractor]s associated with the current list of + * [filteredSubscriptions] + */ + val icons: StateFlow<List<MobileIconInteractor>> + /** True if the active mobile data subscription has data enabled */ val activeDataConnectionHasDataEnabled: StateFlow<Boolean> @@ -259,6 +265,13 @@ constructor( } } + override val icons = + filteredSubscriptions + .mapLatest { subs -> + subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the * validated bit from the old active network (A) while data is changing to the new one (B). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt new file mode 100644 index 000000000000..ad8b8100f14d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.data + +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import kotlinx.coroutines.flow.Flow + +/** + * Device-based satellite refers to the capability of a device to connect directly to a satellite + * network. This is in contrast to carrier-based satellite connectivity, which is a property of a + * given mobile data subscription. + */ +interface DeviceBasedSatelliteRepository { + /** See [SatelliteConnectionState] for available states */ + val connectionState: Flow<SatelliteConnectionState> + + /** 0-4 level (similar to wifi and mobile) */ + // @IntRange(from = 0, to = 4) + val signalStrength: Flow<Int> + + /** Clients must observe this property, as device-based satellite is location-dependent */ + val isSatelliteAllowedForCurrentLocation: Flow<Boolean> +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt new file mode 100644 index 000000000000..8fc8b2f31366 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.data.prod + +import android.os.OutcomeReceiver +import android.telephony.satellite.NtnSignalStrengthCallback +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteStateCallback +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported +import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported +import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported +import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.util.kotlin.getOrNull +import com.android.systemui.util.time.SystemClock +import java.util.Optional +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +/** + * A SatelliteManager that has responded that it has satellite support. Use [SatelliteSupport] to + * get one + */ +private typealias SupportedSatelliteManager = SatelliteManager + +/** + * "Supported" here means supported by the device. The value of this should be stable during the + * process lifetime. + */ +private sealed interface SatelliteSupport { + /** Not yet fetched */ + data object Unknown : SatelliteSupport + + /** + * SatelliteManager says that this mode is supported. Note that satellite manager can never be + * null now + */ + data class Supported(val satelliteManager: SupportedSatelliteManager) : SatelliteSupport + + /** + * Either we were told that there is no support for this feature, or the manager is null, or + * some other exception occurred while querying for support. + */ + data object NotSupported : SatelliteSupport + + @OptIn(ExperimentalCoroutinesApi::class) + companion object { + /** Convenience function to switch to the supported flow */ + fun <T> Flow<SatelliteSupport>.whenSupported( + supported: (SatelliteManager) -> Flow<T>, + orElse: Flow<T>, + ): Flow<T> = flatMapLatest { + when (it) { + is Supported -> supported(it.satelliteManager) + else -> orElse + } + } + } +} + +/** + * Basically your everyday run-of-the-mill system service listener, with three notable exceptions. + * + * First, there is an availability bit that we are tracking via [SatelliteManager]. See + * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about + * this bit is that there is no callback that exists. Therefore we implement a simple polling + * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see + * [POLLING_INTERVAL_MS]) and see what the current state is. + * + * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See + * [SatelliteSupport] for details on how we track the state. What's worth noting here is that + * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental + * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method + * [SatelliteSupport.whenSupported]. By defining flows as simple functions based on a + * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told + * us that satellite is supported. Therefore, we don't expect exceptions to be thrown. + * + * Lastly, this class is designed to wait a full minute of process uptime before making any requests + * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that + * is still booting up or anything like that. We can tune or remove this behavior in the future if + * necessary. + */ +@SysUISingleton +class DeviceBasedSatelliteRepositoryImpl +@Inject +constructor( + satelliteManagerOpt: Optional<SatelliteManager>, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val systemClock: SystemClock, +) : DeviceBasedSatelliteRepository { + + private val satelliteManager: SatelliteManager? + + override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean> + + // Some calls into satellite manager will throw exceptions if it is not supported. + // This is never expected to change after boot, but may need to be retried in some cases + private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown) + + init { + satelliteManager = satelliteManagerOpt.getOrNull() + + isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) + + if (satelliteManager != null) { + // First, check that satellite is supported on this device + scope.launch { + ensureMinUptime(systemClock, MIN_UPTIME) + satelliteSupport.value = satelliteManager.checkSatelliteSupported() + + // We only need to check location availability if this mode is supported + if (satelliteSupport.value is Supported) { + isSatelliteAllowedForCurrentLocation.subscriptionCount + .map { it > 0 } + .distinctUntilChanged() + .collectLatest { hasSubscribers -> + if (hasSubscribers) { + /* + * As there is no listener available for checking satellite allowed, + * we must poll. Defaulting to polling at most once every hour while + * active. Subsequent OOS events will restart the job, so a flaky + * connection might cause more frequent checks. + */ + while (true) { + checkIsSatelliteAllowed() + delay(POLLING_INTERVAL_MS) + } + } + } + } + } + } else { + satelliteSupport.value = NotSupported + } + } + + override val connectionState = + satelliteSupport.whenSupported( + supported = ::connectionStateFlow, + orElse = flowOf(SatelliteConnectionState.Off) + ) + + // By using the SupportedSatelliteManager here, we expect registration never to fail + private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = + conflatedCallbackFlow { + val cb = SatelliteStateCallback { state -> + trySend(SatelliteConnectionState.fromModemState(state)) + } + + sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) + + awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) } + } + .flowOn(bgDispatcher) + + override val signalStrength = + satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0)) + + // By using the SupportedSatelliteManager here, we expect registration never to fail + private fun signalStrengthFlow(sm: SupportedSatelliteManager) = + conflatedCallbackFlow { + val cb = NtnSignalStrengthCallback { signalStrength -> + trySend(signalStrength.level) + } + + sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) + + awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) } + } + .flowOn(bgDispatcher) + + /** Fire off a request to check for satellite availability. Always runs on the bg context */ + private suspend fun checkIsSatelliteAllowed() = + withContext(bgDispatcher) { + satelliteManager?.requestIsSatelliteCommunicationAllowedForCurrentLocation( + bgDispatcher.asExecutor(), + object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { + override fun onError(e: SatelliteManager.SatelliteException) { + android.util.Log.e(TAG, "Found exception when checking for satellite: ", e) + isSatelliteAllowedForCurrentLocation.value = false + } + + override fun onResult(allowed: Boolean) { + isSatelliteAllowedForCurrentLocation.value = allowed + } + } + ) + } + + private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport = + suspendCancellableCoroutine { continuation -> + val cb = + object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { + override fun onResult(supported: Boolean) { + continuation.resume( + if (supported) { + Supported(satelliteManager = this@checkSatelliteSupported) + } else { + NotSupported + } + ) + } + + override fun onError(error: SatelliteManager.SatelliteException) { + // Assume that an error means it's not supported + continuation.resume(NotSupported) + } + } + + requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb) + } + + companion object { + // TTL for satellite polling is one hour + const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60 + + // Let the system boot up and stabilize before we check for system support + const val MIN_UPTIME: Long = 1000 * 60 + + private const val TAG = "DeviceBasedSatelliteRepo" + + /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */ + private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) { + val timeTilMinUptime = + uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis()) + if (timeTilMinUptime > 0) { + delay(timeTilMinUptime) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt new file mode 100644 index 000000000000..877957733d67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.domain.interactor + +import com.android.internal.telephony.flags.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class DeviceBasedSatelliteInteractor +@Inject +constructor( + val repo: DeviceBasedSatelliteRepository, + iconsInteractor: MobileIconsInteractor, + @Application scope: CoroutineScope, +) { + /** Must be observed by any UI showing Satellite iconography */ + val isSatelliteAllowed = + if (Flags.oemEnabledSatelliteFlag()) { + repo.isSatelliteAllowedForCurrentLocation + } else { + flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + /** See [SatelliteConnectionState] for relevant states */ + val connectionState = + if (Flags.oemEnabledSatelliteFlag()) { + repo.connectionState + } else { + + flowOf(SatelliteConnectionState.Off) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off) + + /** 0-4 description of the connection strength */ + val signalStrength = + if (Flags.oemEnabledSatelliteFlag()) { + repo.signalStrength + } else { + flowOf(0) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + + /** When all connections are considered OOS, satellite connectivity is potentially valid */ + val areAllConnectionsOutOfService = + if (Flags.oemEnabledSatelliteFlag()) { + iconsInteractor.icons.aggregateOver(selector = { intr -> intr.isInService }) { + isInServiceList -> + isInServiceList.all { !it } + } + } else { + flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) +} + +/** + * aggregateOver allows us to combine over the leaf-nodes of successive lists emitted from the + * top-level flow. Re-emits if the list changes, or any of the intermediate values change. + * + * Provides a way to connect the reactivity of the top-level flow with the reactivity of an + * arbitrarily-defined relationship ([selector]) from R to the flow that R exposes. + */ +@OptIn(ExperimentalCoroutinesApi::class) +private inline fun <R, reified S, T> Flow<List<R>>.aggregateOver( + crossinline selector: (R) -> Flow<S>, + crossinline transform: (Array<S>) -> T +): Flow<T> { + return map { list -> list.map { selector(it) } } + .flatMapLatest { newFlows -> combine(newFlows) { newVals -> transform(newVals) } } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt new file mode 100644 index 000000000000..bfe294119e64 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.shared.model + +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN + +enum class SatelliteConnectionState { + // State is unknown or undefined + Unknown, + // Radio is off + Off, + // Radio is on, but not yet connected + On, + // Radio is connected, aka satellite is available for use + Connected; + + companion object { + // TODO(b/316635648): validate these states. We don't need the level of granularity that + // SatelliteManager gives us. + fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) = + when (modemState) { + // Transferring data is connected + SATELLITE_MODEM_STATE_CONNECTED, + SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING, + SATELLITE_MODEM_STATE_DATAGRAM_RETRYING -> Connected + + // Modem is on but not connected + SATELLITE_MODEM_STATE_IDLE, + SATELLITE_MODEM_STATE_LISTENING, + SATELLITE_MODEM_STATE_NOT_CONNECTED -> On + + // Consider unavailable equivalent to Off + SATELLITE_MODEM_STATE_UNAVAILABLE, + SATELLITE_MODEM_STATE_OFF -> Off + + // Else, we don't know what's up + SATELLITE_MODEM_STATE_UNKNOWN -> Unknown + else -> Unknown + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt b/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt index ac04d31041b6..4f7dce363a2b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ReferenceExt.kt @@ -2,6 +2,7 @@ package com.android.systemui.util import java.lang.ref.SoftReference import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicReference import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -48,3 +49,25 @@ fun <T> softReference(obj: T? = null): ReadWriteProperty<Any?, T?> { } } } + +/** + * Creates a nullable Kotlin idiomatic [AtomicReference]. + * + * Usage: + * ``` + * var atomicReferenceObj: Object? by nullableAtomicReference(null) + * atomicReferenceObj = Object() + * ``` + */ +fun <T> nullableAtomicReference(obj: T? = null): ReadWriteProperty<Any?, T?> { + return object : ReadWriteProperty<Any?, T?> { + val t = AtomicReference(obj) + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + return t.get() + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + t.set(value) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java index df5162af70c5..3d724e1caa5d 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java @@ -22,12 +22,17 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import android.util.IndentingPrintWriter; import android.util.Log; +import androidx.annotation.NonNull; + import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.annotations.WeaklyReferencedCallback; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; @@ -244,6 +249,21 @@ public class ObservableServiceConnection<T> implements ServiceConnection { }); } + void dump(@NonNull PrintWriter pw) { + IndentingPrintWriter ipw = DumpUtilsKt.asIndenting(pw); + ipw.println("ObservableServiceConnection state:"); + DumpUtilsKt.withIncreasedIndent(ipw, () -> { + ipw.println("mServiceIntent: " + mServiceIntent); + ipw.println("mLastDisconnectReason: " + mLastDisconnectReason.orElse(-1)); + ipw.println("Callbacks:"); + DumpUtilsKt.withIncreasedIndent(ipw, () -> { + for (WeakReference<Callback<T>> cbRef : mCallbacks) { + ipw.println(cbRef.get()); + } + }); + }); + } + private void applyToCallbacksLocked(Consumer<Callback<T>> applicator) { final Iterator<WeakReference<Callback<T>>> iterator = mCallbacks.iterator(); diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java index 6e19bed49626..9b72eb710588 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java @@ -17,6 +17,7 @@ package com.android.systemui.util.service; import static com.android.systemui.util.service.dagger.ObservableServiceModule.BASE_RECONNECT_DELAY_MS; +import static com.android.systemui.util.service.dagger.ObservableServiceModule.DUMPSYS_NAME; import static com.android.systemui.util.service.dagger.ObservableServiceModule.MAX_RECONNECT_ATTEMPTS; import static com.android.systemui.util.service.dagger.ObservableServiceModule.MIN_CONNECTION_DURATION_MS; import static com.android.systemui.util.service.dagger.ObservableServiceModule.OBSERVER; @@ -24,9 +25,15 @@ import static com.android.systemui.util.service.dagger.ObservableServiceModule.S import android.util.Log; +import androidx.annotation.NonNull; + +import com.android.systemui.Dumpable; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.time.SystemClock; +import java.io.PrintWriter; + import javax.inject.Inject; import javax.inject.Named; @@ -35,7 +42,7 @@ import javax.inject.Named; * {@link ObservableServiceConnection}. * @param <T> The transformed connection type handled by the service. */ -public class PersistentConnectionManager<T> { +public class PersistentConnectionManager<T> implements Dumpable { private static final String TAG = "PersistentConnManager"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -45,6 +52,8 @@ public class PersistentConnectionManager<T> { private final int mMaxReconnectAttempts; private final int mMinConnectionDuration; private final Observer mObserver; + private final DumpManager mDumpManager; + private final String mDumpsysName; private int mReconnectAttempts = 0; private Runnable mCurrentReconnectCancelable; @@ -89,6 +98,8 @@ public class PersistentConnectionManager<T> { public PersistentConnectionManager( SystemClock clock, DelayableExecutor mainExecutor, + DumpManager dumpManager, + @Named(DUMPSYS_NAME) String dumpsysName, @Named(SERVICE_CONNECTION) ObservableServiceConnection<T> serviceConnection, @Named(MAX_RECONNECT_ATTEMPTS) int maxReconnectAttempts, @Named(BASE_RECONNECT_DELAY_MS) int baseReconnectDelayMs, @@ -98,6 +109,8 @@ public class PersistentConnectionManager<T> { mMainExecutor = mainExecutor; mConnection = serviceConnection; mObserver = observer; + mDumpManager = dumpManager; + mDumpsysName = TAG + "#" + dumpsysName; mMaxReconnectAttempts = maxReconnectAttempts; mBaseReconnectDelayMs = baseReconnectDelayMs; @@ -108,6 +121,7 @@ public class PersistentConnectionManager<T> { * Begins the {@link PersistentConnectionManager} by connecting to the associated service. */ public void start() { + mDumpManager.registerCriticalDumpable(mDumpsysName, this); mConnection.addCallback(mConnectionCallback); mObserver.addCallback(mObserverCallback); initiateConnectionAttempt(); @@ -120,6 +134,32 @@ public class PersistentConnectionManager<T> { mConnection.removeCallback(mConnectionCallback); mObserver.removeCallback(mObserverCallback); mConnection.unbind(); + mDumpManager.unregisterDumpable(mDumpsysName); + } + + /** + * Add a callback to the {@link ObservableServiceConnection}. + * @param callback The callback to add. + */ + public void addConnectionCallback(ObservableServiceConnection.Callback<T> callback) { + mConnection.addCallback(callback); + } + + /** + * Remove a callback from the {@link ObservableServiceConnection}. + * @param callback The callback to remove. + */ + public void removeConnectionCallback(ObservableServiceConnection.Callback<T> callback) { + mConnection.removeCallback(callback); + } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("mMaxReconnectAttempts: " + mMaxReconnectAttempts); + pw.println("mBaseReconnectDelayMs: " + mBaseReconnectDelayMs); + pw.println("mMinConnectionDuration: " + mMinConnectionDuration); + pw.println("mReconnectAttempts: " + mReconnectAttempts); + mConnection.dump(pw); } private void initiateConnectionAttempt() { diff --git a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java index bcf34f833d32..c52c524d1fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java @@ -19,14 +19,14 @@ package com.android.systemui.util.service.dagger; import android.content.res.Resources; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Main; - -import javax.inject.Named; +import com.android.systemui.res.R; import dagger.Module; import dagger.Provides; +import javax.inject.Named; + /** * Module containing components and parameters for * {@link com.android.systemui.util.service.ObservableServiceConnection} @@ -41,6 +41,7 @@ public class ObservableServiceModule { public static final String MIN_CONNECTION_DURATION_MS = "min_connection_duration_ms"; public static final String SERVICE_CONNECTION = "service_connection"; public static final String OBSERVER = "observer"; + public static final String DUMPSYS_NAME = "dumpsys_name"; @Provides @Named(MAX_RECONNECT_ATTEMPTS) diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt index 342494d8997b..46936d6223a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository import com.android.systemui.decor.FaceScanningProviderFactory import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.log.logcatLogBuffer @@ -53,6 +54,8 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + private val facePropertyRepository = FakeFacePropertyRepository() + private val displayId = 2 @Before @@ -86,9 +89,10 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { keyguardUpdateMonitor, mock(Executor::class.java), ScreenDecorationsLogger(logcatLogBuffer("FaceScanningProviderFactoryTest")), + facePropertyRepository, ) - whenever(authController.faceSensorLocation).thenReturn(Point(10, 10)) + facePropertyRepository.setSensorLocation(Point(10, 10)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index c094df5dd569..c07148b32cf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -54,6 +54,7 @@ import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Path; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; @@ -80,6 +81,7 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository; import com.android.systemui.decor.CornerDecorProvider; import com.android.systemui.decor.CutoutDecorProviderFactory; import com.android.systemui.decor.CutoutDecorProviderImpl; @@ -101,6 +103,7 @@ import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; @@ -108,8 +111,6 @@ import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; @@ -169,8 +170,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener; @Mock private CutoutDecorProviderFactory mCutoutFactory; - @Captor - private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback; + @Mock + private JavaAdapter mJavaAdapter; + + private FakeFacePropertyRepository mFakeFacePropertyRepository = + new FakeFacePropertyRepository(); private List<DecorProvider> mMockCutoutList; @Before @@ -227,20 +231,23 @@ public class ScreenDecorationsTest extends SysuiTestCase { doAnswer(it -> !(mMockCutoutList.isEmpty())).when(mCutoutFactory).getHasProviders(); doReturn(mMockCutoutList).when(mCutoutFactory).getProviders(); + mFakeFacePropertyRepository.setSensorLocation(new Point(10, 10)); + mFaceScanningDecorProvider = spy(new FaceScanningOverlayProviderImpl( BOUNDS_POSITION_TOP, mAuthController, mStatusBarStateController, mKeyguardUpdateMonitor, mExecutor, - new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")))); + new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), + mFakeFacePropertyRepository)); mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), - mAuthController) { + mFakeFacePropertyRepository, mJavaAdapter) { @Override public void start() { super.start(); @@ -1235,9 +1242,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, - new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController); + new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), + mFakeFacePropertyRepository, mJavaAdapter); screenDecorations.start(); - verify(mAuthController).addCallback(mAuthControllerCallback.capture()); when(mContext.getDisplay()).thenReturn(mDisplay); when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() { @Override @@ -1252,9 +1259,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { }); mExecutor.runAllReady(); clearInvocations(mFaceScanningDecorProvider); - - AuthController.Callback callback = mAuthControllerCallback.getValue(); - callback.onFaceSensorLocationChanged(); + final Point location = new Point(); + mFakeFacePropertyRepository.setSensorLocation(location); + screenDecorations.onFaceSensorLocationChanged(location); mExecutor.runAllReady(); verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/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/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 313276727caf..a20658197a8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -152,7 +152,9 @@ import com.android.systemui.statusbar.notification.ConversationNotificationManag import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger; +import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository; import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -586,6 +588,10 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); + NotificationsKeyguardViewStateRepository notifsKeyguardViewStateRepository = + new NotificationsKeyguardViewStateRepository(); + NotificationsKeyguardInteractor notifsKeyguardInteractor = + new NotificationsKeyguardInteractor(notifsKeyguardViewStateRepository); NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( mDumpManager, @@ -596,7 +602,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardBypassController, mDozeParameters, mScreenOffAnimationController, - new NotificationWakeUpCoordinatorLogger(logcatLogBuffer())); + new NotificationWakeUpCoordinatorLogger(logcatLogBuffer()), + notifsKeyguardInteractor); mConfigurationController = new ConfigurationControllerImpl(mContext); PulseExpansionHandler expansionHandler = new PulseExpansionHandler( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 438b33d9afbc..039fef9c1df5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -22,12 +22,14 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.dump.DumpManager +import com.android.systemui.kosmos.Kosmos import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeViewController.Companion.WAKEUP_ANIMATION_DELAY_MS import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_WAKEUP +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController @@ -54,6 +56,8 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule() + private val kosmos = Kosmos() + private val dumpManager: DumpManager = mock() private val headsUpManager: HeadsUpManager = mock() private val statusBarStateController: StatusBarStateController = mock() @@ -100,6 +104,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { dozeParameters, screenOffAnimationController, logger, + kosmos.notificationsKeyguardInteractor, ) statusBarStateCallback = withArgCaptor { verify(statusBarStateController).addCallback(capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt deleted file mode 100644 index 170f651aed91..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.data.repository - -import androidx.test.filters.SmallTest -import com.android.systemui.SysUITestComponent -import com.android.systemui.SysUITestModule -import com.android.systemui.SysuiTestCase -import com.android.systemui.collectLastValue -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.runCurrent -import com.android.systemui.runTest -import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.mockito.withArgCaptor -import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance -import dagger.Component -import org.junit.Test -import org.mockito.Mockito.verify - -@SmallTest -class NotificationsKeyguardViewStateRepositoryTest : SysuiTestCase() { - - @SysUISingleton - @Component(modules = [SysUITestModule::class]) - interface TestComponent : SysUITestComponent<NotificationsKeyguardViewStateRepositoryImpl> { - - val mockWakeUpCoordinator: NotificationWakeUpCoordinator - - @Component.Factory - interface Factory { - fun create( - @BindsInstance test: SysuiTestCase, - ): TestComponent - } - } - - private val testComponent: TestComponent = - DaggerNotificationsKeyguardViewStateRepositoryTest_TestComponent.factory() - .create(test = this) - - @Test - fun areNotifsFullyHidden_reflectsWakeUpCoordinator() = - testComponent.runTest { - whenever(mockWakeUpCoordinator.notificationsFullyHidden).thenReturn(false) - val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden) - runCurrent() - - assertThat(notifsFullyHidden).isFalse() - - withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) } - .onFullyHiddenChanged(true) - runCurrent() - - assertThat(notifsFullyHidden).isTrue() - } - - @Test - fun isPulseExpanding_reflectsWakeUpCoordinator() = - testComponent.runTest { - whenever(mockWakeUpCoordinator.isPulseExpanding()).thenReturn(false) - val isPulseExpanding by collectLastValue(underTest.isPulseExpanding) - runCurrent() - - assertThat(isPulseExpanding).isFalse() - - withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) } - .onPulseExpandingChanged(true) - runCurrent() - - assertThat(isPulseExpanding).isTrue() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt index bb3113a72e92..3593f5b4963e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt @@ -21,7 +21,6 @@ import com.android.systemui.collectLastValue import com.android.systemui.dagger.SysUISingleton import com.android.systemui.runCurrent import com.android.systemui.runTest -import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -33,9 +32,6 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { @SysUISingleton @Component(modules = [SysUITestModule::class]) interface TestComponent : SysUITestComponent<NotificationsKeyguardInteractor> { - - val repository: FakeNotificationsKeyguardViewStateRepository - @Component.Factory interface Factory { fun create(@BindsInstance test: SysuiTestCase): TestComponent @@ -48,13 +44,13 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { @Test fun areNotifsFullyHidden_reflectsRepository() = testComponent.runTest { - repository.setNotificationsFullyHidden(false) + underTest.setNotificationsFullyHidden(false) val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden) runCurrent() assertThat(notifsFullyHidden).isFalse() - repository.setNotificationsFullyHidden(true) + underTest.setNotificationsFullyHidden(true) runCurrent() assertThat(notifsFullyHidden).isTrue() @@ -63,13 +59,13 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { @Test fun isPulseExpanding_reflectsRepository() = testComponent.runTest { - repository.setPulseExpanding(false) + underTest.setPulseExpanding(false) val isPulseExpanding by collectLastValue(underTest.isPulseExpanding) runCurrent() assertThat(isPulseExpanding).isFalse() - repository.setPulseExpanding(true) + underTest.setPulseExpanding(true) runCurrent() assertThat(isPulseExpanding).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index 47feccf4bdcf..7faf5628b40a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -29,8 +29,8 @@ import com.android.systemui.statusbar.data.repository.NotificationListenerSettin import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore -import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.notification.shared.byIsAmbient import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply import com.android.systemui.statusbar.notification.shared.byIsPulsing @@ -61,7 +61,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { interface TestComponent : SysUITestComponent<NotificationIconsInteractor> { val activeNotificationListRepository: ActiveNotificationListRepository - val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository + val notificationsKeyguardInteractor: NotificationsKeyguardInteractor @Component.Factory interface Factory { @@ -136,7 +136,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { fun filteredEntrySet_noPulsing_notifsNotFullyHidden() = testComponent.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false)) - keyguardViewStateRepository.setNotificationsFullyHidden(false) + notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true) } @@ -144,7 +144,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { fun filteredEntrySet_noPulsing_notifsFullyHidden() = testComponent.runTest { val filteredSet by collectLastValue(underTest.filteredNotifSet(showPulsing = false)) - keyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } } @@ -161,7 +161,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { val activeNotificationListRepository: ActiveNotificationListRepository val deviceEntryRepository: FakeDeviceEntryRepository - val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository + val notificationsKeyguardInteractor: NotificationsKeyguardInteractor @Component.Factory interface Factory { @@ -222,7 +222,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(false) - keyguardViewStateRepository.setNotificationsFullyHidden(false) + notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } @@ -231,7 +231,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(false) - keyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } @@ -240,7 +240,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(true) - keyguardViewStateRepository.setNotificationsFullyHidden(false) + notificationsKeyguardInteractor.setNotificationsFullyHidden(false) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).doesNotContain(true) } @@ -249,7 +249,7 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { testComponent.runTest { val filteredSet by collectLastValue(underTest.aodNotifs) deviceEntryRepository.setBypassEnabled(true) - keyguardViewStateRepository.setNotificationsFullyHidden(true) + notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } } @@ -266,7 +266,7 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { val activeNotificationListRepository: ActiveNotificationListRepository val headsUpIconsInteractor: HeadsUpNotificationIconInteractor - val keyguardViewStateRepository: FakeNotificationsKeyguardViewStateRepository + val notificationsKeyguardInteractor: NotificationsKeyguardInteractor val notificationListenerSettingsRepository: NotificationListenerSettingsRepository @Component.Factory diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt index 6f04f369d52f..f6a8243c7a46 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt @@ -23,6 +23,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat @@ -49,14 +52,15 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { iconList = StatusBarIconList(arrayOf()) underTest = StatusBarIconControllerImpl( - context, - commandQueue, - mock(), - mock(), - mock(), - mock(), - iconList, - mock(), + /* context = */ context, + /* commandQueue = */ commandQueue, + /* demoModeController = */ mock(), + /* configurationController = */ mock(), + /* tunerService = */ mock(), + /* dumpManager = */ mock(), + /* statusBarIconList = */ iconList, + /* statusBarPipelineFlags = */ mock(), + /* modernIconsRegistry = */ mock(), ) underTest.addIconGroup(iconGroup) val commandQueueCallbacksCaptor = kotlinArgumentCaptor<CommandQueue.Callbacks>() @@ -366,6 +370,31 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX") } + @Test + fun bindableIcons_addedOnInit() { + val fakeIcon = FakeBindableIcon("test_slot") + + iconList = StatusBarIconList(arrayOf()) + + // WHEN there are registered icons + underTest = + StatusBarIconControllerImpl( + /* context = */ context, + /* commandQueue = */ commandQueue, + /* demoModeController = */ mock(), + /* configurationController = */ mock(), + /* tunerService = */ mock(), + /* dumpManager = */ mock(), + /* statusBarIconList = */ iconList, + /* statusBarPipelineFlags = */ mock(), + /* modernIconsRegistry = */ FakeBindableIconsRegistry(listOf(fakeIcon)), + ) + + // THEN they are properly added to the list on init + assertThat(iconList.getIconHolder("test_slot", 0)) + .isInstanceOf(StatusBarIconHolder.BindableIconHolder::class.java) + } + private fun createExternalIcon(): StatusBarIcon { return StatusBarIcon( "external.package", @@ -377,3 +406,20 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { ) } } + +class FakeBindableIconsRegistry( + override val bindableIcons: List<BindableIcon>, +) : BindableIconsRegistry + +class FakeBindableIcon( + override val slot: String, + override val shouldBindIcon: Boolean = true, +) : BindableIcon { + // Track initialized so we can know that our icon was properly bound + var hasInitialized = false + + override val initializer = ModernStatusBarViewCreator { _ -> + hasInitialized = true + mock() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java index 0ff6f200d402..ca316230fb01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; @@ -108,7 +109,8 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { mock(TunerService.class), mock(DumpManager.class), mock(StatusBarIconList.class), - flags + flags, + mock(BindableIconsRegistry.class) ); iconController.addIconGroup(manager); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt new file mode 100644 index 000000000000..a906a8953e02 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.data.prod + +import android.os.OutcomeReceiver +import android.os.Process +import android.telephony.satellite.NtnSignalStrength +import android.telephony.satellite.NtnSignalStrengthCallback +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE +import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN +import android.telephony.satellite.SatelliteManager.SatelliteException +import android.telephony.satellite.SatelliteStateCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME +import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { + private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl + + @Mock private lateinit var satelliteManager: SatelliteManager + + private val systemClock = FakeSystemClock() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun nullSatelliteManager_usesDefaultValues() = + testScope.runTest { + setupDefaultRepo() + underTest = + DeviceBasedSatelliteRepositoryImpl( + Optional.empty(), + dispatcher, + testScope.backgroundScope, + systemClock, + ) + + val connectionState by collectLastValue(underTest.connectionState) + val strength by collectLastValue(underTest.signalStrength) + val allowed by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(connectionState).isEqualTo(SatelliteConnectionState.Off) + assertThat(strength).isEqualTo(0) + assertThat(allowed).isFalse() + } + + @Test + fun connectionState_mapsFromSatelliteModemState() = + testScope.runTest { + setupDefaultRepo() + val latest by collectLastValue(underTest.connectionState) + runCurrent() + val callback = + withArgCaptor<SatelliteStateCallback> { + verify(satelliteManager).registerForSatelliteModemStateChanged(any(), capture()) + } + + // Mapping from modem state to SatelliteConnectionState is rote, just run all of the + // possibilities here + + // Off states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF) + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNAVAILABLE) + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // On states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_IDLE) + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_LISTENING) + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_NOT_CONNECTED) + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + + // Connected states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_CONNECTED) + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING) + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_RETRYING) + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + + // Unknown states + callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNKNOWN) + assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown) + // Garbage value (for completeness' sake) + callback.onSatelliteModemStateChanged(123456) + assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown) + } + + @Test + fun signalStrength_readsSatelliteManagerState() = + testScope.runTest { + setupDefaultRepo() + val latest by collectLastValue(underTest.signalStrength) + runCurrent() + val callback = + withArgCaptor<NtnSignalStrengthCallback> { + verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture()) + } + + assertThat(latest).isNull() + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(1)) + assertThat(latest).isEqualTo(1) + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(2)) + assertThat(latest).isEqualTo(2) + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(3)) + assertThat(latest).isEqualTo(3) + + callback.onNtnSignalStrengthChanged(NtnSignalStrength(4)) + assertThat(latest).isEqualTo(4) + } + + @Test + fun isSatelliteAllowed_readsSatelliteManagerState_enabled() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is allowed in this location + val allowed = true + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isTrue() + } + + @Test + fun isSatelliteAllowed_readsSatelliteManagerState_disabled() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is not allowed in this location + val allowed = false + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isFalse() + } + + @Test + fun isSatelliteAllowed_pollsOnTimeout() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is not allowed in this location + var allowed = false + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isFalse() + + // WHEN satellite becomes enabled + allowed = true + + // WHEN the timeout has not yet been reached + advanceTimeBy(POLLING_INTERVAL_MS / 2) + + // THEN the value is still false + assertThat(latest).isFalse() + + // WHEN time advances beyond the polling interval + advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1) + + // THEN then new value is emitted + assertThat(latest).isTrue() + } + + @Test + fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() = + testScope.runTest { + setupDefaultRepo() + // Use the old school launch/cancel so we can simulate subscribers arriving and leaving + + var latest: Boolean? = false + var job = + underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this) + + // GIVEN satellite is not allowed in this location + var allowed = false + + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onResult(allowed) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + assertThat(latest).isFalse() + + // WHEN satellite becomes enabled + allowed = true + + // WHEN the job is restarted + advanceTimeBy(POLLING_INTERVAL_MS / 2) + + job.cancel() + job = + underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this) + + // THEN the value is re-fetched + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isSatelliteAllowed_falseWhenErrorOccurs() = + testScope.runTest { + setupDefaultRepo() + doAnswer { + val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException> + receiver.onError(SatelliteException(1 /* unused */)) + null + } + .`when`(satelliteManager) + .requestIsSatelliteCommunicationAllowedForCurrentLocation( + any(), + any<OutcomeReceiver<Boolean, SatelliteException>>() + ) + + val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation) + + assertThat(latest).isFalse() + } + + @Test + fun satelliteNotSupported_listenersAreNotRegistered() = + testScope.runTest { + setupDefaultRepo() + // GIVEN satellite is not supported + setUpRepo( + uptime = MIN_UPTIME, + satMan = satelliteManager, + satelliteSupported = false, + ) + + // WHEN data is requested from the repo + val connectionState by collectLastValue(underTest.connectionState) + val signalStrength by collectLastValue(underTest.signalStrength) + + // THEN the manager is not asked for the information, and default values are returned + verify(satelliteManager, never()).registerForSatelliteModemStateChanged(any(), any()) + verify(satelliteManager, never()).registerForNtnSignalStrengthChanged(any(), any()) + } + + @Test + fun repoDoesNotCheckForSupportUntilMinUptime() = + testScope.runTest { + // GIVEN we init 100ms after sysui starts up + setUpRepo( + uptime = 100, + satMan = satelliteManager, + satelliteSupported = true, + ) + + // WHEN data is requested + val connectionState by collectLastValue(underTest.connectionState) + val signalStrength by collectLastValue(underTest.signalStrength) + + // THEN we have not yet talked to satellite manager, since we are well before MIN_UPTIME + Mockito.verifyZeroInteractions(satelliteManager) + + // WHEN enough time has passed + systemClock.advanceTime(MIN_UPTIME) + runCurrent() + + // THEN we finally register with the satellite manager + verify(satelliteManager).registerForSatelliteModemStateChanged(any(), any()) + } + + private fun setUpRepo( + uptime: Long = MIN_UPTIME, + satMan: SatelliteManager? = satelliteManager, + satelliteSupported: Boolean = true, + ) { + doAnswer { + val callback: OutcomeReceiver<Boolean, SatelliteException> = + it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException> + callback.onResult(satelliteSupported) + } + .whenever(satelliteManager) + .requestIsSatelliteSupported(any(), any()) + + systemClock.setUptimeMillis(Process.getStartUptimeMillis() + uptime) + + underTest = + DeviceBasedSatelliteRepositoryImpl( + if (satMan != null) Optional.of(satMan) else Optional.empty(), + dispatcher, + testScope.backgroundScope, + systemClock, + ) + } + + // Set system time to MIN_UPTIME and create a repo with satellite supported + private fun setupDefaultRepo() { + setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt new file mode 100644 index 000000000000..5fa2d33c9de0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.data.prod + +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState.Off +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeDeviceBasedSatelliteRepository() : DeviceBasedSatelliteRepository { + override val connectionState = MutableStateFlow(Off) + + override val signalStrength = MutableStateFlow(0) + + override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt new file mode 100644 index 000000000000..e010b8672580 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before + +@SmallTest +class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { + private lateinit var underTest: DeviceBasedSatelliteInteractor + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private val iconsInteractor = + FakeMobileIconsInteractor( + FakeMobileMappingsProxy(), + mock(), + ) + + private val repo = FakeDeviceBasedSatelliteRepository() + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + } + + @Test + fun isSatelliteAllowed_falseWhenNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.isSatelliteAllowed) + + // WHEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // THEN the interactor returns false due to the flag value + assertThat(latest).isFalse() + } + + @Test + fun isSatelliteAllowed_trueWhenAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.isSatelliteAllowed) + + // WHEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // THEN the interactor returns false due to the flag value + assertThat(latest).isTrue() + } + + @Test + fun isSatelliteAllowed_offWhenFlagIsOff() = + testScope.runTest { + // GIVEN feature is disabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.isSatelliteAllowed) + + // WHEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // THEN the interactor returns false due to the flag value + assertThat(latest).isFalse() + } + + @Test + fun connectionState_matchesRepositoryValue() = + testScope.runTest { + val latest by collectLastValue(underTest.connectionState) + + // Off + repo.connectionState.value = SatelliteConnectionState.Off + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // On + repo.connectionState.value = SatelliteConnectionState.On + assertThat(latest).isEqualTo(SatelliteConnectionState.On) + + // Connected + repo.connectionState.value = SatelliteConnectionState.Connected + assertThat(latest).isEqualTo(SatelliteConnectionState.Connected) + + // Unknown + repo.connectionState.value = SatelliteConnectionState.Unknown + assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown) + } + + @Test + fun connectionState_offWhenFeatureIsDisabled() = + testScope.runTest { + // GIVEN the flag is disabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.connectionState) + + // THEN the state is always Off, regardless of status in system_server + + // Off + repo.connectionState.value = SatelliteConnectionState.Off + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // On + repo.connectionState.value = SatelliteConnectionState.On + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // Connected + repo.connectionState.value = SatelliteConnectionState.Connected + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + + // Unknown + repo.connectionState.value = SatelliteConnectionState.Unknown + assertThat(latest).isEqualTo(SatelliteConnectionState.Off) + } + + @Test + fun signalStrength_matchesRepo() = + testScope.runTest { + val latest by collectLastValue(underTest.signalStrength) + + repo.signalStrength.value = 1 + assertThat(latest).isEqualTo(1) + + repo.signalStrength.value = 2 + assertThat(latest).isEqualTo(2) + + repo.signalStrength.value = 3 + assertThat(latest).isEqualTo(3) + + repo.signalStrength.value = 4 + assertThat(latest).isEqualTo(4) + } + + @Test + fun signalStrength_zeroWhenDisabled() = + testScope.runTest { + // GIVEN the flag is enabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.signalStrength) + + // THEN the value is always 0, regardless of what the system says + repo.signalStrength.value = 1 + assertThat(latest).isEqualTo(0) + + repo.signalStrength.value = 2 + assertThat(latest).isEqualTo(0) + + repo.signalStrength.value = 3 + assertThat(latest).isEqualTo(0) + + repo.signalStrength.value = 4 + assertThat(latest).isEqualTo(0) + } + + @Test + fun areAllConnectionsOutOfService_twoConnectionsOos_yes() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 2 connections + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2) + + // WHEN all of the connections are OOS + i1.isInService.value = false + i2.isInService.value = false + + // THEN the value is propagated to this interactor + assertThat(latest).isTrue() + } + + @Test + fun areAllConnectionsOutOfService_oneConnectionOos_yes() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 1 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + // WHEN all of the connections are OOS + i1.isInService.value = false + + // THEN the value is propagated to this interactor + assertThat(latest).isTrue() + } + + @Test + fun areAllConnectionsOutOfService_oneConnectionInService_no() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 1 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + // WHEN all of the connections are NOT OOS + i1.isInService.value = true + + // THEN the value is propagated to this interactor + assertThat(latest).isFalse() + } + + @Test + fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 2 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2) + + // WHEN at least 1 connection is NOT OOS. + i1.isInService.value = false + i2.isInService.value = true + + // THEN the value is propagated to this interactor + assertThat(latest).isFalse() + } + + @Test + fun areAllConnectionsOutOfService_twoConnectionsInService_no() = + testScope.runTest { + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN, 2 connection + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + // WHEN all connections are NOT OOS. + i1.isInService.value = true + i2.isInService.value = true + + // THEN the value is propagated to this interactor + assertThat(latest).isFalse() + } + + @Test + fun areAllConnectionsOutOfService_falseWhenFlagIsOff() = + testScope.runTest { + // GIVEN the flag is disabled + mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG) + + // Remake the interactor so the flag is read + underTest = + DeviceBasedSatelliteInteractor( + repo, + iconsInteractor, + testScope.backgroundScope, + ) + + val latest by collectLastValue(underTest.areAllConnectionsOutOfService) + + // GIVEN a condition that should return true (all conections OOS) + + val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1) + + i1.isInService.value = true + i2.isInService.value = true + + // THEN the value is still false, because the flag is off + assertThat(latest).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java index db0139c9b0d1..55c49ee4360d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java @@ -24,6 +24,7 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.DumpManager; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -41,6 +42,7 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { private static final int MAX_RETRIES = 5; private static final int RETRY_DELAY_MS = 1000; private static final int CONNECTION_MIN_DURATION_MS = 5000; + private static final String DUMPSYS_NAME = "dumpsys_name"; private FakeSystemClock mFakeClock = new FakeSystemClock(); private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock); @@ -49,8 +51,14 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { private ObservableServiceConnection<Proxy> mConnection; @Mock + private ObservableServiceConnection.Callback<Proxy> mConnectionCallback; + + @Mock private Observer mObserver; + @Mock + private DumpManager mDumpManager; + private static class Proxy { } @@ -63,6 +71,8 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { mConnectionManager = new PersistentConnectionManager<>( mFakeClock, mFakeExecutor, + mDumpManager, + DUMPSYS_NAME, mConnection, MAX_RETRIES, RETRY_DELAY_MS, @@ -154,4 +164,16 @@ public class PersistentConnectionManagerTest extends SysuiTestCase { callbackCaptor.getValue().onSourceChanged(); verify(mConnection).bind(); } + + @Test + public void testAddConnectionCallback() { + mConnectionManager.addConnectionCallback(mConnectionCallback); + verify(mConnection).addCallback(mConnectionCallback); + } + + @Test + public void testRemoveConnectionCallback() { + mConnectionManager.removeConnectionCallback(mConnectionCallback); + verify(mConnection).removeCallback(mConnectionCallback); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt index 3fdeb302dc34..3b5ff38e3663 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.data.repository +import android.util.Size import com.android.systemui.biometrics.shared.model.DisplayRotation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,6 +30,9 @@ class FakeDisplayStateRepository : DisplayStateRepository { private val _currentRotation = MutableStateFlow<DisplayRotation>(DisplayRotation.ROTATION_0) override val currentRotation: StateFlow<DisplayRotation> = _currentRotation.asStateFlow() + private val _currentDisplaySize = MutableStateFlow<Size>(Size(0, 0)) + override val currentDisplaySize: StateFlow<Size> = _currentDisplaySize.asStateFlow() + override val isReverseDefaultRotation = false fun setIsInRearDisplayMode(isInRearDisplayMode: Boolean) { @@ -38,4 +42,8 @@ class FakeDisplayStateRepository : DisplayStateRepository { fun setCurrentRotation(currentRotation: DisplayRotation) { _currentRotation.value = currentRotation } + + fun setCurrentDisplaySize(size: Size) { + _currentDisplaySize.value = size + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt index 51ce9f00a709..77f501f550d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.data.repository +import android.graphics.Point import com.android.systemui.biometrics.shared.model.LockoutMode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -28,6 +29,10 @@ class FakeFacePropertyRepository : FacePropertyRepository { private val lockoutModesForUser = mutableMapOf<Int, LockoutMode>() + private val faceSensorLocation = MutableStateFlow<Point?>(null) + override val sensorLocation: StateFlow<Point?> + get() = faceSensorLocation + fun setLockoutMode(userId: Int, mode: LockoutMode) { lockoutModesForUser[userId] = mode } @@ -38,4 +43,8 @@ class FakeFacePropertyRepository : FacePropertyRepository { fun setSensorInfo(value: FaceSensorInfo?) { faceSensorInfo.value = value } + + fun setSensorLocation(value: Point?) { + faceSensorLocation.value = value + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index c85c27e277b4..e82cae45c8f0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn @@ -52,4 +53,12 @@ class FakeCommunalRepository( fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) { _isCommunalHubShowing.value = isCommunalHubShowing } + + private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) + override val isCtaTileInViewModeVisible: Flow<Boolean> = + _isCtaTileInViewModeVisible.asStateFlow() + + override fun setCtaTileInViewModeVisibility(isVisible: Boolean) { + _isCtaTileInViewModeVisible.value = isVisible + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index c6f12e2014b3..397dc1a464bd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -1,15 +1,38 @@ package com.android.systemui.communal.data.repository +import android.content.ComponentName import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch /** Fake implementation of [CommunalWidgetRepository] */ -class FakeCommunalWidgetRepository : CommunalWidgetRepository { +class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : + CommunalWidgetRepository { private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList()) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets + private val _widgetAdded = MutableSharedFlow<Int>() + val widgetAdded: Flow<Int> = _widgetAdded + + private var nextWidgetId = 1 fun setCommunalWidgets(inventory: List<CommunalWidgetContentModel>) { _communalWidgets.value = inventory } + + override fun addWidget( + provider: ComponentName, + priority: Int, + configureWidget: suspend (id: Int) -> Boolean + ) { + coroutineScope.launch { + val id = nextWidgetId++ + if (configureWidget.invoke(id)) { + _widgetAdded.emit(id) + } + } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt index faacce64b2e4..eb287ee522c0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt @@ -36,7 +36,8 @@ object CommunalInteractorFactory { fun create( testScope: TestScope = TestScope(), communalRepository: FakeCommunalRepository = FakeCommunalRepository(), - widgetRepository: FakeCommunalWidgetRepository = FakeCommunalWidgetRepository(), + widgetRepository: FakeCommunalWidgetRepository = + FakeCommunalWidgetRepository(testScope.backgroundScope), mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(), smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt index 788e3aa9c41a..1ffc9f4e30b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt @@ -15,8 +15,6 @@ */ package com.android.systemui.statusbar.notification.data -import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardStateRepositoryModule import dagger.Module -@Module(includes = [FakeNotificationsKeyguardStateRepositoryModule::class]) -object FakeStatusBarNotificationsDataLayerModule +@Module(includes = []) object FakeStatusBarNotificationsDataLayerModule diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt deleted file mode 100644 index 5d3cb4db9c7e..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.statusbar.notification.data.repository - -import com.android.systemui.dagger.SysUISingleton -import dagger.Binds -import dagger.Module -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -@SysUISingleton -class FakeNotificationsKeyguardViewStateRepository @Inject constructor() : - NotificationsKeyguardViewStateRepository { - private val _notificationsFullyHidden = MutableStateFlow(false) - override val areNotificationsFullyHidden: Flow<Boolean> = _notificationsFullyHidden - - private val _isPulseExpanding = MutableStateFlow(false) - override val isPulseExpanding: Flow<Boolean> = _isPulseExpanding - - fun setNotificationsFullyHidden(fullyHidden: Boolean) { - _notificationsFullyHidden.value = fullyHidden - } - - fun setPulseExpanding(expanding: Boolean) { - _isPulseExpanding.value = expanding - } -} - -@Module -interface FakeNotificationsKeyguardStateRepositoryModule { - @Binds - fun bindFake( - fake: FakeNotificationsKeyguardViewStateRepository - ): NotificationsKeyguardViewStateRepository -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt index f2b9da413c22..df7fd94d19b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryKosmos.kt @@ -18,7 +18,5 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.kosmos.Kosmos -var Kosmos.notificationsKeyguardViewStateRepository: NotificationsKeyguardViewStateRepository by - Kosmos.Fixture { fakeNotificationsKeyguardViewStateRepository } -val Kosmos.fakeNotificationsKeyguardViewStateRepository by - Kosmos.Fixture { FakeNotificationsKeyguardViewStateRepository() } +val Kosmos.notificationsKeyguardViewStateRepository: NotificationsKeyguardViewStateRepository by + Kosmos.Fixture { NotificationsKeyguardViewStateRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt index 432464e86c3f..61a38b864c40 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt @@ -18,13 +18,11 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor val Kosmos.notificationsKeyguardInteractor by Fixture { NotificationsKeyguardInteractor( repository = notificationsKeyguardViewStateRepository, - backgroundDispatcher = testDispatcher, ) } diff --git a/packages/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/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index c72632fb367d..c2bc1e4f6be2 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -893,7 +893,7 @@ public class SoundDoseHelper { if (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC && safeDevicesContains(device)) { soundDose.updateAttenuation( - AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC, + -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC, (newIndex + 5) / 10, device), device); } diff --git a/services/core/java/com/android/server/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/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index bd22e1d21dea..4c4cf6080965 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -16,6 +16,7 @@ package com.android.server.display; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS; import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat; import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat; @@ -567,7 +568,8 @@ public class DisplayDeviceConfig { public static final int DEFAULT_LOW_REFRESH_RATE = 60; - private static final float BRIGHTNESS_DEFAULT = 0.5f; + @VisibleForTesting + static final float BRIGHTNESS_DEFAULT = 0.5f; private static final String ETC_DIR = "etc"; private static final String DISPLAY_CONFIG_DIR = "displayconfig"; private static final String CONFIG_FILE_FORMAT = "display_%s.xml"; @@ -597,8 +599,6 @@ public class DisplayDeviceConfig { // so -2 is used instead private static final float INVALID_BRIGHTNESS_IN_CONFIG = -2f; - static final float NITS_INVALID = -1; - // Length of the ambient light horizon used to calculate the long term estimate of ambient // light. private static final int AMBIENT_LIGHT_LONG_HORIZON_MILLIS = 10000; @@ -1031,11 +1031,12 @@ public class DisplayDeviceConfig { /** * Calculates the nits value for the specified backlight value if a mapping exists. * - * @return The mapped nits or {@link #NITS_INVALID} if no mapping exits. + * @return The mapped nits or {@link BrightnessMappingStrategy.INVALID_NITS} if no mapping + * exits. */ public float getNitsFromBacklight(float backlight) { if (mBacklightToNitsSpline == null) { - return NITS_INVALID; + return INVALID_NITS; } backlight = Math.max(backlight, mBacklightMinimum); return mBacklightToNitsSpline.interpolate(backlight); @@ -1061,7 +1062,7 @@ public class DisplayDeviceConfig { float backlight = getBacklightFromBrightness(brightness); float nits = getNitsFromBacklight(backlight); - if (nits == NITS_INVALID) { + if (nits == INVALID_NITS) { return PowerManager.BRIGHTNESS_INVALID; } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index bc3f9dd3cb8c..fbac924be283 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1615,6 +1615,10 @@ public final class DisplayManagerService extends SystemService { if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) { Slog.w(TAG, "Display created with home support but lacks " + "VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the home support request."); + } else if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { + Slog.w(TAG, "Display created with home support but has " + + "VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, ignoring the home support " + + "request."); } else { mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId, Display.TYPE_VIRTUAL, true); diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 7df61142475a..2d860c0cc673 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -573,10 +573,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mBrightnessClamperController = mInjector.getBrightnessClamperController( mHandler, modeChangeCallback::run, new BrightnessClamperController.DisplayDeviceData( - mUniqueDisplayId, - mThermalBrightnessThrottlingDataId, - logicalDisplay.getPowerThrottlingDataIdLocked(), - mDisplayDeviceConfig), mContext, flags); + mUniqueDisplayId, + mThermalBrightnessThrottlingDataId, + logicalDisplay.getPowerThrottlingDataIdLocked(), + mDisplayDeviceConfig), mContext, flags); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); mAutomaticBrightnessStrategy = @@ -1508,7 +1508,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // Note throttling effectively changes the allowed brightness range, so, similarly to HBM, // we broadcast this change through setting. final float unthrottledBrightnessState = brightnessState; - DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest, brightnessState, slowChange); @@ -1522,11 +1521,12 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal if (updateScreenBrightnessSetting) { // Tell the rest of the system about the new brightness in case we had to change it // for things like auto-brightness or high-brightness-mode. Note that we do this - // only considering maxBrightness (ignroing brightness modifiers like low power or dim) + // only considering maxBrightness (ignoring brightness modifiers like low power or dim) // so that the slider accurately represents the full possible range, // even if they range changes what it means in absolute terms. mDisplayBrightnessController.updateScreenBrightnessSetting( - Math.min(unthrottledBrightnessState, clampedState.getMaxBrightness())); + MathUtils.constrain(unthrottledBrightnessState, + clampedState.getMinBrightness(), clampedState.getMaxBrightness())); } // The current brightness to use has been calculated at this point, and HbmController should @@ -1935,8 +1935,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal @Nullable DisplayBrightnessState state) { synchronized (mCachedBrightnessInfo) { float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX; - final float minBrightness = Math.min( - mBrightnessRangeController.getCurrentBrightnessMin(), stateMax); + float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX; + final float minBrightness = Math.max(stateMin, Math.min( + mBrightnessRangeController.getCurrentBrightnessMin(), stateMax)); final float maxBrightness = Math.min( mBrightnessRangeController.getCurrentBrightnessMax(), stateMax); boolean changed = false; @@ -1962,7 +1963,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal changed |= mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason, mBrightnessClamperController.getBrightnessMaxReason()); - return changed; } } @@ -2880,6 +2880,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0, mBrightnessClamperController.getBrightnessMaxReason(), + // TODO: (flc) add brightnessMinReason here too. (modifier & BrightnessReason.MODIFIER_DIMMED) > 0, event.isRbcEnabled(), (flags & BrightnessEvent.FLAG_INVALID_LUX) > 0, diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java index bcf27b4e8f0a..90bad12869f4 100644 --- a/services/core/java/com/android/server/display/DisplayPowerState.java +++ b/services/core/java/com/android/server/display/DisplayPowerState.java @@ -333,6 +333,8 @@ final class DisplayPowerState { public void stop() { mStopped = true; mPhotonicModulator.interrupt(); + mColorFadePrepared = false; + mColorFadeReady = true; if (mColorFade != null) { mAsyncDestroyExecutor.execute(mColorFade::destroy); } @@ -419,7 +421,8 @@ final class DisplayPowerState { } }; - private final Runnable mColorFadeDrawRunnable = new Runnable() { + @VisibleForTesting + final Runnable mColorFadeDrawRunnable = new Runnable() { @Override public void run() { mColorFadeDrawPending = false; diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 25576ce9efd6..3a6333099b77 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -19,6 +19,8 @@ package com.android.server.display; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.Mode.INVALID_MODE_ID; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS; + import android.annotation.Nullable; import android.app.ActivityThread; import android.content.Context; @@ -956,8 +958,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { void handleHdrSdrNitsChanged(float displayNits, float sdrNits) { final float newHdrSdrRatio; - if (displayNits != DisplayDeviceConfig.NITS_INVALID - && sdrNits != DisplayDeviceConfig.NITS_INVALID) { + if (displayNits != INVALID_NITS && sdrNits != INVALID_NITS) { // Ensure the ratio stays >= 1.0f as values below that are nonsensical newHdrSdrRatio = Math.max(1.f, displayNits / sdrNits); } else { diff --git a/services/core/java/com/android/server/display/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/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/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 992d8eb8b1bb..dd9541e5dda1 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -175,6 +175,7 @@ import com.android.server.SystemConfig; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; +import com.android.server.criticalevents.CriticalEventLog; import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; @@ -957,6 +958,7 @@ final class InstallPackageHelper { final Set<String> scannedPackages = new ArraySet<>(requests.size()); final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size()); final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size()); + CriticalEventLog.getInstance().logInstallPackagesStarted(); boolean success = false; try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI"); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 376b06105b8c..a4af5e71000c 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -27,6 +27,7 @@ import static android.content.pm.ArchivedActivityInfo.drawableToBitmap; import static android.content.pm.PackageInstaller.EXTRA_UNARCHIVE_STATUS; import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION; import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; +import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageManager.DELETE_ARCHIVE; import static android.content.pm.PackageManager.DELETE_KEEP_DATA; import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; @@ -100,6 +101,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; /** @@ -210,7 +212,6 @@ public class PackageArchiver { return; } - // TODO(b/278553670) Add special strings for the delete dialog mPm.mInstallerService.uninstall( new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), @@ -264,7 +265,7 @@ public class PackageArchiver { try { // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options. requestUnarchive(packageName, callerPackageName, - getOrCreateUnarchiveIntentSender(userId, packageName), + getOrCreateLauncherListener(userId, packageName), UserHandle.of(userId), false /* showUnarchivalConfirmation= */); } catch (Throwable t) { @@ -329,7 +330,7 @@ public class PackageArchiver { return true; } - private IntentSender getOrCreateUnarchiveIntentSender(int userId, String packageName) { + private IntentSender getOrCreateLauncherListener(int userId, String packageName) { Pair<Integer, String> key = Pair.create(userId, packageName); synchronized (mLauncherIntentSenders) { IntentSender intentSender = mLauncherIntentSenders.get(key); @@ -515,7 +516,6 @@ public class PackageArchiver { /** * Returns true if the app is archivable. */ - // TODO(b/299299569) Exclude system apps public boolean isAppArchivable(@NonNull String packageName, @NonNull UserHandle user) { Objects.requireNonNull(packageName); Objects.requireNonNull(user); @@ -685,15 +685,14 @@ public class PackageArchiver { PackageInstaller.SessionParams.MODE_FULL_INSTALL); sessionParams.setAppPackageName(packageName); sessionParams.installFlags = INSTALL_UNARCHIVE_DRAFT; - sessionParams.unarchiveIntentSender = statusReceiver; int installerUid = mPm.snapshotComputer().getPackageUid(installerPackage, 0, userId); // Handles case of repeated unarchival calls for the same package. - // TODO(b/316881759) Allow attaching multiple intentSenders to one session. int existingSessionId = mPm.mInstallerService.getExistingDraftSessionId(installerUid, sessionParams, userId); if (existingSessionId != PackageInstaller.SessionInfo.INVALID_ID) { + attachListenerToSession(statusReceiver, existingSessionId, userId); return existingSessionId; } @@ -702,12 +701,34 @@ public class PackageArchiver { installerPackage, mContext.getAttributionTag(), installerUid, userId); + attachListenerToSession(statusReceiver, sessionId, userId); + // TODO(b/297358628) Also cleanup sessions upon device restart. mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), getUnarchiveForegroundTimeout()); return sessionId; } + private void attachListenerToSession(IntentSender statusReceiver, int existingSessionId, + int userId) { + PackageInstallerSession session = mPm.mInstallerService.getSession(existingSessionId); + int status = session.getUnarchivalStatus(); + // Here we handle a race condition that might happen when an installer reports UNARCHIVAL_OK + // but hasn't created a session yet. Without this the listener would never receive a success + // response. + if (status == UNARCHIVAL_OK) { + notifyUnarchivalListener(UNARCHIVAL_OK, session.getInstallerPackageName(), + session.params.appPackageName, /* requiredStorageBytes= */ 0, + /* userActionIntent= */ null, Set.of(statusReceiver), userId); + return; + } else if (status != UNARCHIVAL_STATUS_UNSET) { + throw new IllegalStateException(TextUtils.formatSimple("Session %s has unarchive status" + + "%s but is still active.", session.sessionId, status)); + } + + session.registerUnarchivalListener(statusReceiver); + } + /** * Returns the icon of an archived app. This is the icon of the main activity of the app. * @@ -883,13 +904,7 @@ public class PackageArchiver { void notifyUnarchivalListener(int status, String installerPackageName, String appPackageName, long requiredStorageBytes, @Nullable PendingIntent userActionIntent, - @Nullable IntentSender unarchiveIntentSender, int userId) { - if (unarchiveIntentSender == null) { - // Maybe this can happen if the installer calls reportUnarchivalStatus twice in quick - // succession. - return; - } - + Set<IntentSender> unarchiveIntentSenders, int userId) { final Intent broadcastIntent = new Intent(); broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, appPackageName); broadcastIntent.putExtra(EXTRA_UNARCHIVE_STATUS, status); @@ -909,15 +924,16 @@ public class PackageArchiver { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); - try { - unarchiveIntentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null, - /* handler= */ null, /* requiredPermission= */ null, - options.toBundle()); - } catch (IntentSender.SendIntentException e) { - Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); - } finally { - synchronized (mLauncherIntentSenders) { - mLauncherIntentSenders.remove(Pair.create(userId, appPackageName)); + for (IntentSender intentSender : unarchiveIntentSenders) { + try { + intentSender.sendIntent(mContext, 0, broadcastIntent, /* onFinished= */ null, + /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); + } catch (IntentSender.SendIntentException e) { + Slog.e(TAG, TextUtils.formatSimple("Failed to send unarchive intent"), e); + } finally { + synchronized (mLauncherIntentSenders) { + mLauncherIntentSenders.remove(Pair.create(userId, appPackageName)); + } } } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 0a23dfb80f3b..a9118d46b4ba 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1759,26 +1759,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements binderUid, unarchiveId)); } - IntentSender unarchiveIntentSender = session.params.unarchiveIntentSender; - if (unarchiveIntentSender == null) { - throw new IllegalStateException( - TextUtils.formatSimple( - "Unarchival status for ID %s has already been set or a " - + "session has been created for it already by the " - + "caller.", - unarchiveId)); - } - - // Execute expensive calls outside the sync block. - mPm.mHandler.post( - () -> mPackageArchiver.notifyUnarchivalListener(status, - session.getInstallerPackageName(), - session.params.appPackageName, requiredStorageBytes, userActionIntent, - unarchiveIntentSender, userId)); - session.params.unarchiveIntentSender = null; - if (status != UNARCHIVAL_OK) { - Binder.withCleanCallingIdentity(session::abandon); - } + session.reportUnarchivalStatus(unarchiveId, status, requiredStorageBytes, + userActionIntent); } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 4adb60c34c52..117d03fd059b 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -22,6 +22,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_UPDAT import static android.content.pm.DataLoaderType.INCREMENTAL; import static android.content.pm.DataLoaderType.STREAMING; import static android.content.pm.PackageInstaller.LOCATION_DATA_APP; +import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; +import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; @@ -65,6 +67,7 @@ import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.Notification; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; @@ -97,6 +100,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.PreapprovalDetails; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; +import android.content.pm.PackageInstaller.UnarchivalStatus; import android.content.pm.PackageInstaller.UserActionReason; import android.content.pm.PackageManager; import android.content.pm.PackageManager.PackageInfoFlags; @@ -771,6 +775,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final List<String> mResolvedInstructionSets = new ArrayList<>(); @GuardedBy("mLock") private final List<String> mResolvedNativeLibPaths = new ArrayList<>(); + + @GuardedBy("mLock") + private final Set<IntentSender> mUnarchivalListeners = new ArraySet<>(); + @GuardedBy("mLock") private File mInheritedFilesBase; @GuardedBy("mLock") @@ -796,6 +804,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private int mValidatedTargetSdk = INVALID_TARGET_SDK_VERSION; + @UnarchivalStatus + private int mUnarchivalStatus = UNARCHIVAL_STATUS_UNSET; + private static final FileFilter sAddedApkFilter = new FileFilter() { @Override public boolean accept(File file) { @@ -5088,6 +5099,44 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + void registerUnarchivalListener(IntentSender intentSender) { + synchronized (mLock) { + this.mUnarchivalListeners.add(intentSender); + } + } + + Set<IntentSender> getUnarchivalListeners() { + synchronized (mLock) { + return new ArraySet<>(mUnarchivalListeners); + } + } + + void reportUnarchivalStatus(@UnarchivalStatus int status, int unarchiveId, + long requiredStorageBytes, PendingIntent userActionIntent) { + if (getUnarchivalStatus() != UNARCHIVAL_STATUS_UNSET) { + throw new IllegalStateException( + TextUtils.formatSimple( + "Unarchival status for ID %s has already been set or a session has " + + "been created for it already by the caller.", + unarchiveId)); + } + mUnarchivalStatus = status; + + // Execute expensive calls outside the sync block. + mPm.mHandler.post( + () -> mPm.mInstallerService.mPackageArchiver.notifyUnarchivalListener(status, + getInstallerPackageName(), params.appPackageName, requiredStorageBytes, + userActionIntent, getUnarchivalListeners(), userId)); + if (status != UNARCHIVAL_OK) { + Binder.withCleanCallingIdentity(this::abandon); + } + } + + @UnarchivalStatus + int getUnarchivalStatus() { + return this.mUnarchivalStatus; + } + /** * Free up storage used by this session and its children. * Must not be called on a child session. diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java index d05e4c69427e..cf5de897cf5d 100644 --- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java +++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java @@ -48,6 +48,7 @@ import java.util.function.BiFunction; class PackageMonitorCallbackHelper { private static final boolean DEBUG = false; + private static final String TAG = "PackageMonitorCallbackHelper"; @NonNull private final Object mLock = new Object(); @@ -243,25 +244,33 @@ class PackageMonitorCallbackHelper { return; } int registerUid = registerUser.getUid(); + if (allowUids != null && registerUid != Process.SYSTEM_UID + && !ArrayUtils.contains(allowUids, registerUid)) { + if (DEBUG) { + Slog.w(TAG, "Skip invoke PackageMonitorCallback for " + intent.getAction() + + ", uid " + registerUid); + } + return; + } + Intent newIntent = intent; if (filterExtrasFunction != null) { final Bundle extras = intent.getExtras(); if (extras != null) { final Bundle filteredExtras = filterExtrasFunction.apply(registerUid, extras); - if (filteredExtras != null) { - intent.replaceExtras(filteredExtras); + if (filteredExtras == null) { + // caller is unable to access this intent + if (DEBUG) { + Slog.w(TAG, + "Skip invoke PackageMonitorCallback for " + intent.getAction() + + " because null filteredExtras"); + } + return; } + newIntent = new Intent(newIntent); + newIntent.replaceExtras(filteredExtras); } } - if (allowUids != null && registerUid != Process.SYSTEM_UID - && !ArrayUtils.contains(allowUids, registerUid)) { - if (DEBUG) { - Slog.w("PackageMonitorCallbackHelper", - "Skip invoke PackageMonitorCallback for " + intent.getAction() - + ", uid " + registerUid); - } - return; - } - invokeCallback(callback, intent); + invokeCallback(callback, newIntent); })); } diff --git a/services/core/java/com/android/server/pm/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/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index b0b66cfa247f..5c867017f4e0 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -130,6 +130,28 @@ class WallpaperData { */ final Rect cropHint = new Rect(0, 0, 0, 0); + // Describes the context of a call to WallpaperManagerService#bindWallpaperComponentLocked + enum BindSource { + UNKNOWN, + CONNECT_LOCKED, + CONNECTION_TRY_TO_REBIND, + INITIALIZE_FALLBACK, + PACKAGE_UPDATE_FINISHED, + RESTORE_SETTINGS_LIVE_FAILURE, + RESTORE_SETTINGS_LIVE_SUCCESS, + RESTORE_SETTINGS_STATIC, + SET_LIVE, + SET_LIVE_TO_CLEAR, + SET_STATIC, + SWITCH_WALLPAPER_FAILURE, + SWITCH_WALLPAPER_SWITCH_USER, + SWITCH_WALLPAPER_UNLOCK_USER, + } + + // Context in which this wallpaper was bound. Intended for use in resolving b/301073479 but may + // be useful after the issue is resolved as well. + BindSource mBindSource = BindSource.UNKNOWN; + // map of which -> File private final SparseArray<File> mWallpaperFiles = new SparseArray<>(); private final SparseArray<File> mCropFiles = new SparseArray<>(); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index 5f8bbe5f18ad..de98df55c3ea 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -46,6 +46,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.JournaledFile; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.wallpaper.WallpaperData.BindSource; import libcore.io.IoUtils; @@ -314,6 +315,14 @@ class WallpaperDataParser { wpData.mPadding.right = getAttributeInt(parser, "paddingRight", 0); wpData.mPadding.bottom = getAttributeInt(parser, "paddingBottom", 0); wallpaper.mWallpaperDimAmount = getAttributeFloat(parser, "dimAmount", 0f); + BindSource bindSource; + try { + bindSource = Enum.valueOf(BindSource.class, + getAttributeString(parser, "bindSource", BindSource.UNKNOWN.name())); + } catch (IllegalArgumentException | NullPointerException e) { + bindSource = BindSource.UNKNOWN; + } + wallpaper.mBindSource = bindSource; int dimAmountsCount = getAttributeInt(parser, "dimAmountsCount", 0); if (dimAmountsCount > 0) { SparseArray<Float> allDimAmounts = new SparseArray<>(dimAmountsCount); @@ -364,6 +373,11 @@ class WallpaperDataParser { return parser.getAttributeFloat(null, name, defValue); } + private String getAttributeString(XmlPullParser parser, String name, String defValue) { + String s = parser.getAttributeValue(null, name); + return (s != null) ? s : defValue; + } + void saveSettingsLocked(int userId, WallpaperData wallpaper, WallpaperData lockWallpaper) { JournaledFile journal = makeJournaledFile(userId); FileOutputStream fstream = null; @@ -423,6 +437,7 @@ class WallpaperDataParser { } out.attributeFloat(null, "dimAmount", wallpaper.mWallpaperDimAmount); + out.attribute(null, "bindSource", wallpaper.mBindSource.name()); int dimAmountsCount = wallpaper.mUidToDimAmount.size(); out.attributeInt(null, "dimAmountsCount", dimAmountsCount); if (dimAmountsCount > 0) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 1485b961789c..3782b429f93a 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -122,6 +122,7 @@ import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; +import com.android.server.wallpaper.WallpaperData.BindSource; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -335,6 +336,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub }; // If this was the system wallpaper, rebind... + wallpaper.mBindSource = BindSource.SET_STATIC; bindWallpaperComponentLocked(mImageWallpaper, true, false, wallpaper, callback); } @@ -354,6 +356,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } }; + wallpaper.mBindSource = BindSource.SET_STATIC; bindWallpaperComponentLocked(mImageWallpaper, true /* force */, false /* fromUser */, wallpaper, callback); } else if (isAppliedToLock) { @@ -811,6 +814,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.w(TAG, "Failed attaching wallpaper on display", e); if (wallpaper != null && !wallpaper.wallpaperUpdating && connection.getConnectedEngineSize() == 0) { + wallpaper.mBindSource = BindSource.CONNECT_LOCKED; bindWallpaperComponentLocked(null /* componentName */, false /* force */, false /* fromUser */, wallpaper, null /* reply */); } @@ -1035,6 +1039,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final ComponentName wpService = mWallpaper.wallpaperComponent; // The broadcast of package update could be delayed after service disconnected. Try // to re-bind the service for 10 seconds. + mWallpaper.mBindSource = BindSource.CONNECTION_TRY_TO_REBIND; if (bindWallpaperComponentLocked( wpService, true, false, mWallpaper, null)) { mWallpaper.connection.scheduleTimeoutLocked(); @@ -1321,6 +1326,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } wallpaper.wallpaperUpdating = false; clearWallpaperComponentLocked(wallpaper); + wallpaper.mBindSource = BindSource.PACKAGE_UPDATE_FINISHED; if (!bindWallpaperComponentLocked(wpService, false, false, wallpaper, null)) { Slog.w(TAG, "Wallpaper " + wpService @@ -1711,6 +1717,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (mHomeWallpaperWaitingForUnlock) { final WallpaperData systemWallpaper = getWallpaperSafeLocked(userId, FLAG_SYSTEM); + systemWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER; switchWallpaper(systemWallpaper, null); // TODO(b/278261563): call notifyCallbacksLocked inside switchWallpaper notifyCallbacksLocked(systemWallpaper); @@ -1718,6 +1725,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (mLockWallpaperWaitingForUnlock) { final WallpaperData lockWallpaper = getWallpaperSafeLocked(userId, FLAG_LOCK); + lockWallpaper.mBindSource = BindSource.SWITCH_WALLPAPER_UNLOCK_USER; switchWallpaper(lockWallpaper, null); notifyCallbacksLocked(lockWallpaper); } @@ -1838,6 +1846,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // delete them in order to show the default wallpaper. clearWallpaperBitmaps(wallpaper); + fallback.mBindSource = BindSource.SWITCH_WALLPAPER_FAILURE; bindWallpaperComponentLocked(mImageWallpaper, true, false, fallback, reply); if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = true; if ((wallpaper.mWhich & FLAG_LOCK) != 0) mLockWallpaperWaitingForUnlock = true; @@ -2963,6 +2972,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub */ boolean forceRebind = force || (same && systemIsBoth && which == FLAG_SYSTEM); + newWallpaper.mBindSource = + (name == null) ? BindSource.SET_LIVE_TO_CLEAR : BindSource.SET_LIVE; bindSuccess = bindWallpaperComponentLocked(name, /* force */ forceRebind, /* fromUser */ true, newWallpaper, reply); if (bindSuccess) { @@ -3530,6 +3541,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mFallbackWallpaper = new WallpaperData(systemUserId, FLAG_SYSTEM); mFallbackWallpaper.allowBackup = false; mFallbackWallpaper.wallpaperId = makeWallpaperIdLocked(); + mFallbackWallpaper.mBindSource = BindSource.INITIALIZE_FALLBACK; bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false, mFallbackWallpaper, null); } @@ -3553,11 +3565,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.allowBackup = true; // by definition if it was restored if (wallpaper.nextWallpaperComponent != null && !wallpaper.nextWallpaperComponent.equals(mImageWallpaper)) { + wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_SUCCESS; if (!bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, false, false, wallpaper, null)) { // No such live wallpaper or other failure; fall back to the default // live wallpaper (since the profile being restored indicated that the // user had selected a live rather than static one). + wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_FAILURE; bindWallpaperComponentLocked(null, false, false, wallpaper, null); } success = true; @@ -3575,6 +3589,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub + " id=" + wallpaper.wallpaperId); if (success) { mWallpaperCropper.generateCrop(wallpaper); // based on the new image + metadata + wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_STATIC; bindWallpaperComponentLocked(wallpaper.nextWallpaperComponent, true, false, wallpaper, null); } @@ -3608,7 +3623,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub pw.print(" User "); pw.print(wallpaper.userId); pw.print(": id="); pw.print(wallpaper.wallpaperId); pw.print(": mWhich="); pw.print(wallpaper.mWhich); - pw.print(": mSystemWasBoth="); pw.println(wallpaper.mSystemWasBoth); + pw.print(": mSystemWasBoth="); pw.print(wallpaper.mSystemWasBoth); + pw.print(": mBindSource="); pw.println(wallpaper.mBindSource.name()); pw.println(" Display state:"); mWallpaperDisplayHelper.forEachDisplayData(wpSize -> { pw.print(" displayId="); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5fa2610cc17b..3a792d079db2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2495,7 +2495,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SnapshotStartingData"); mStartingData = new SnapshotStartingData(mWmService, snapshot, typeParams); - if (task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) { + if ((!mStyleFillsParent && task.getChildCount() > 1) + || task.forAllLeafTaskFragments(TaskFragment::isEmbedded)) { + // Case 1: + // If it is moving a Task{[0]=main activity, [1]=translucent activity} to front, use + // shared starting window so that the transition doesn't need to wait for the activity + // behind the translucent activity. Also, onFirstWindowDrawn will check all visible + // activities are drawn in the task to remove the snapshot starting window. + // Case 2: // Associate with the task so if this activity is resized by task fragment later, the // starting window can keep the same bounds as the task. associateStartingDataWithTask(); @@ -10616,6 +10623,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { + if (task != null && task.mSharedStartingData != null) { + final WindowState startingWin = task.topStartingWindow(); + if (startingWin != null && startingWin.mSyncState == SYNC_STATE_READY + && mDisplayContent.mUnknownAppVisibilityController.allResolved()) { + // The sync is ready if a drawn starting window covered the task. + return true; + } + } if (!super.isSyncFinished(group)) return false; if (mDisplayContent != null && mDisplayContent.mUnknownAppVisibilityController .isVisibilityUnknown(this)) { diff --git a/services/core/java/com/android/server/wm/SensitiveContentPackages.java b/services/core/java/com/android/server/wm/SensitiveContentPackages.java new file mode 100644 index 000000000000..3862b82512c3 --- /dev/null +++ b/services/core/java/com/android/server/wm/SensitiveContentPackages.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import android.annotation.NonNull; +import android.util.ArraySet; + +import java.io.PrintWriter; +import java.util.Objects; +import java.util.Set; + +/** + * Cache of distinct package/uid pairs that require being blocked from screen capture. This class is + * not threadsafe and any call site should hold {@link WindowManagerGlobalLock} + */ +public class SensitiveContentPackages { + private final ArraySet<PackageInfo> mProtectedPackages = new ArraySet<>(); + + /** Returns {@code true} if package/uid pair should be blocked from screen capture */ + public boolean shouldBlockScreenCaptureForApp(String pkg, int uid) { + for (int i = 0; i < mProtectedPackages.size(); i++) { + PackageInfo info = mProtectedPackages.valueAt(i); + if (info != null && info.mPkg.equals(pkg) && info.mUid == uid) { + return true; + } + } + return false; + } + + /** Replaces the set of package/uid pairs to set that should be blocked from screen capture */ + public void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos) { + mProtectedPackages.clear(); + mProtectedPackages.addAll(packageInfos); + } + + void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println("SensitiveContentPackages:"); + pw.println(innerPrefix + "Packages that should block screen capture (" + + mProtectedPackages.size() + "):"); + for (PackageInfo info : mProtectedPackages) { + pw.println(innerPrefix + " package=" + info.mPkg + " uid=" + info.mUid); + } + } + + /** Helper class that represents a package/uid pair */ + public static class PackageInfo { + private String mPkg; + private int mUid; + + public PackageInfo(String pkg, int uid) { + this.mPkg = pkg; + this.mUid = uid; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PackageInfo)) return false; + PackageInfo that = (PackageInfo) o; + return mUid == that.mUid && Objects.equals(mPkg, that.mPkg); + } + + @Override + public int hashCode() { + return Objects.hash(mPkg, mUid); + } + } +} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 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/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/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index e370f5501865..c67e7c5ae61e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -41,6 +41,7 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.hardware.display.DisplayManagerInternal; +import android.os.PowerManager; import android.os.Temperature; import android.provider.Settings; import android.util.SparseArray; @@ -109,6 +110,43 @@ public final class DisplayDeviceConfigTest { } @Test + public void testDefaultValues() { + when(mResources.getString(com.android.internal.R.string.config_displayLightSensorType)) + .thenReturn("test_light_sensor"); + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true); + + mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, /* useConfigXml= */ false, + mFlags); + + assertEquals(DisplayDeviceConfig.BRIGHTNESS_DEFAULT, + mDisplayDeviceConfig.getBrightnessDefault(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampFastDecrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampFastIncrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowDecreaseIdle(), ZERO_DELTA); + assertEquals(PowerManager.BRIGHTNESS_MAX, + mDisplayDeviceConfig.getBrightnessRampSlowIncreaseIdle(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis()); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis()); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampDecreaseMaxIdleMillis()); + assertEquals(0, mDisplayDeviceConfig.getBrightnessRampIncreaseMaxIdleMillis()); + assertNull(mDisplayDeviceConfig.getNits()); + assertNull(mDisplayDeviceConfig.getBacklight()); + assertEquals(0.3f, mDisplayDeviceConfig.getBacklightFromBrightness(0.3f), ZERO_DELTA); + assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type); + assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name); + assertNull(mDisplayDeviceConfig.getProximitySensor().type); + assertNull(mDisplayDeviceConfig.getProximitySensor().name); + assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); + } + + @Test public void testConfigValuesFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(); @@ -681,6 +719,7 @@ public final class DisplayDeviceConfigTest { assertEquals("test_light_sensor", mDisplayDeviceConfig.getAmbientLightSensor().type); assertEquals("", mDisplayDeviceConfig.getAmbientLightSensor().name); + assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); assertEquals(brightnessIntToFloat(35), mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA); @@ -807,6 +846,24 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), SMALL_DELTA); } + @Test + public void testIsAutoBrightnessAvailable_EnabledInConfigResource() throws IOException { + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true); + + setupDisplayDeviceConfigFromDisplayConfigFile(); + + assertTrue(mDisplayDeviceConfig.isAutoBrightnessAvailable()); + } + + @Test + public void testIsAutoBrightnessAvailable_DisabledInConfigResource() throws IOException { + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(false); + + setupDisplayDeviceConfigFromDisplayConfigFile(); + + assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable()); + } + private String getValidLuxThrottling() { return "<luxThrottling>\n" + " <brightnessLimitMap>\n" @@ -1176,7 +1233,7 @@ public final class DisplayDeviceConfigTest { + "<nits>" + NITS[2] + "</nits>\n" + "</point>\n" + "</screenBrightnessMap>\n" - + "<autoBrightness>\n" + + "<autoBrightness enabled=\"true\">\n" + "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n" + "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n" + (includeIdleMode ? getRampSpeedsIdle() : "") @@ -1593,6 +1650,7 @@ public final class DisplayDeviceConfigTest { when(mResources.getString(com.android.internal.R.string.config_displayLightSensorType)) .thenReturn("test_light_sensor"); + when(mResources.getBoolean(R.bool.config_automatic_brightness_available)).thenReturn(true); when(mResources.getInteger( R.integer.config_autoBrightnessBrighteningLightDebounce)) diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt index dafbbb3e0140..33d30200faaa 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt @@ -16,16 +16,24 @@ package com.android.server.display +import android.content.Context +import android.os.Looper import android.view.Display import androidx.test.filters.SmallTest import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.util.concurrent.Executor @SmallTest @@ -39,11 +47,16 @@ class DisplayPowerStateTest { private val mockBlanker = mock<DisplayBlanker>() private val mockColorFade = mock<ColorFade>() private val mockExecutor = mock<Executor>() + private val mockContext = mock<Context>() @Before fun setUp() { + if (Looper.myLooper() == null) { + Looper.prepare() + } displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON, mockExecutor) + whenever(mockColorFade.prepare(eq(mockContext), anyInt())).thenReturn(true) } @Test @@ -56,4 +69,31 @@ class DisplayPowerStateTest { verify(mockColorFade).destroy() } + + @Test + fun `GIVEN not prepared WHEN draw runnable is called THEN colorFade not drawn`() { + displayPowerState.mColorFadeDrawRunnable.run() + + verify(mockColorFade, never()).draw(anyFloat()) + } + @Test + fun `GIVEN prepared WHEN draw runnable is called THEN colorFade is drawn`() { + displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE) + clearInvocations(mockColorFade) + + displayPowerState.mColorFadeDrawRunnable.run() + + verify(mockColorFade).draw(anyFloat()) + } + + @Test + fun `GIVEN prepared AND stopped WHEN draw runnable is called THEN colorFade is not drawn`() { + displayPowerState.prepareColorFade(mockContext, ColorFade.MODE_FADE) + clearInvocations(mockColorFade) + displayPowerState.stop() + + displayPowerState.mColorFadeDrawRunnable.run() + + verify(mockColorFade, never()).draw(anyFloat()) + } }
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 00f98924eff3..c92ce254cae4 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -176,6 +176,10 @@ public class LocalDisplayAdapterTest { when(mockArray.length()).thenReturn(0); when(mMockedResources.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray)) .thenReturn(mockArray); + when(mMockedResources.obtainTypedArray(R.array.config_displayCutoutSideOverrideArray)) + .thenReturn(mockArray); + when(mMockedResources.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride)) + .thenReturn(new String[]{}); when(mMockedResources.obtainTypedArray(R.array.config_waterfallCutoutArray)) .thenReturn(mockArray); when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerRadiusArray)) diff --git a/services/tests/displayservicetests/src/com/android/server/display/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/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index e5ecdc478df7..0403c64fc624 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -210,6 +210,9 @@ public class PackageArchiverTest { anyInt())).thenReturn(1); when(mInstallerService.getExistingDraftSessionId(anyInt(), any(), anyInt())).thenReturn( PackageInstaller.SessionInfo.INVALID_ID); + PackageInstallerSession session = mock(PackageInstallerSession.class); + when(mInstallerService.getSession(anyInt())).thenReturn(session); + when(session.getUnarchivalStatus()).thenReturn(PackageInstaller.UNARCHIVAL_STATUS_UNSET); doReturn(new ParceledListSlice<>(List.of(mock(ResolveInfo.class)))) .when(mPackageManagerService).queryIntentReceivers(any(), any(), any(), anyLong(), eq(mUserId)); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java index 60cedcfd6dd0..24e7242792fb 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java @@ -108,6 +108,20 @@ public class PackageMonitorCallbackHelperTest { } @Test + public void testPackageMonitorCallback_SuspendNoAccessCallbackNotCalled() throws Exception { + BiFunction<Integer, Bundle, Bundle> filterExtras = (callingUid, intentExtras) -> null; + + IRemoteCallback callback = createMockPackageMonitorCallback(); + mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */, + Binder.getCallingUid()); + mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGES_SUSPENDED, + FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0}, null /* instantUserIds */, + null /* broadcastAllowList */, mHandler, filterExtras); + + verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any()); + } + + @Test public void testPackageMonitorCallback_SuspendCallbackCalled() throws Exception { Bundle result = new Bundle(); result.putInt(Intent.EXTRA_UID, FAKE_PACKAGE_UID); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 9b8419021c5c..00450267ee79 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -215,6 +215,16 @@ java_library { } java_library { + name: "mockito-test-utils", + srcs: [ + "utils-mockito/**/*.kt", + ], + static_libs: [ + "mockito-target-minus-junit4", + ], +} + +java_library { name: "servicestests-utils-mockito-extended", srcs: [ "utils/**/*.java", diff --git a/services/tests/servicestests/src/com/android/server/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/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 { |