diff options
334 files changed, 12218 insertions, 3509 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index eed248bcd2b9..a80194cf53d2 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -335,6 +335,17 @@ java_aconfig_library { mode: "exported", } +cc_aconfig_library { + name: "android.os.flags-aconfig-cc", + aconfig_declarations: "android.os.flags-aconfig", +} + +cc_aconfig_library { + name: "android.os.flags-aconfig-cc-test", + aconfig_declarations: "android.os.flags-aconfig", + mode: "test", +} + // VirtualDeviceManager cc_aconfig_library { name: "android.companion.virtualdevice.flags-aconfig-cc", @@ -492,6 +503,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "android.content.res.flags-aconfig-java-host", + aconfig_declarations: "android.content.res.flags-aconfig", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media BetterTogether aconfig_declarations { name: "com.android.media.flags.bettertogether-aconfig", diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index e96d07f44b34..ee9400fb8408 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -46,8 +46,11 @@ import android.os.Message; import android.os.PowerManager; import android.os.UserHandle; import android.provider.DeviceConfig; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotMapping; import android.util.ArraySet; import android.util.IndentingPrintWriter; +import android.util.IntArray; import android.util.KeyValueListParser; import android.util.Log; import android.util.Slog; @@ -68,6 +71,8 @@ import com.android.server.utils.AlarmQueue; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Set; import java.util.function.Predicate; /** @@ -1620,9 +1625,21 @@ public final class FlexibilityController extends StateController { private final Object mSatLock = new Object(); private DeviceIdleInternal mDeviceIdleInternal; + private TelephonyManager mTelephonyManager; + + private final boolean mHasFeatureTelephonySubscription; /** Set of all apps that have been deemed special, keyed by user ID. */ private final SparseSetArray<String> mSpecialApps = new SparseSetArray<>(); + /** + * Set of carrier privileged apps, keyed by the logical ID of the SIM their privileged + * for. + */ + @GuardedBy("mSatLock") + private final SparseSetArray<String> mCarrierPrivilegedApps = new SparseSetArray<>(); + @GuardedBy("mSatLock") + private final SparseArray<LogicalIndexCarrierPrivilegesCallback> + mCarrierPrivilegedCallbacks = new SparseArray<>(); @GuardedBy("mSatLock") private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>(); @@ -1630,6 +1647,10 @@ public final class FlexibilityController extends StateController { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { + case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED: + updateCarrierPrivilegedCallbackRegistration(); + break; + case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache); break; @@ -1637,6 +1658,11 @@ public final class FlexibilityController extends StateController { } }; + SpecialAppTracker() { + mHasFeatureTelephonySubscription = mContext.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION); + } + public boolean isSpecialApp(final int userId, @NonNull String packageName) { synchronized (mSatLock) { if (mSpecialApps.contains(UserHandle.USER_ALL, packageName)) { @@ -1654,6 +1680,12 @@ public final class FlexibilityController extends StateController { if (mPowerAllowlistedApps.contains(packageName)) { return true; } + for (int l = mCarrierPrivilegedApps.size() - 1; l >= 0; --l) { + if (mCarrierPrivilegedApps.contains( + mCarrierPrivilegedApps.keyAt(l), packageName)) { + return true; + } + } } return false; } @@ -1669,9 +1701,12 @@ public final class FlexibilityController extends StateController { private void onSystemServicesReady() { mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); synchronized (mLock) { if (mFlexibilityEnabled) { + mHandler.post( + SpecialAppTracker.this::updateCarrierPrivilegedCallbackRegistration); mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache); } } @@ -1686,6 +1721,13 @@ public final class FlexibilityController extends StateController { private void startTracking() { IntentFilter filter = new IntentFilter( PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + + if (mHasFeatureTelephonySubscription) { + filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); + + updateCarrierPrivilegedCallbackRegistration(); + } + mContext.registerReceiver(mBroadcastReceiver, filter); updatePowerAllowlistCache(); @@ -1695,9 +1737,61 @@ public final class FlexibilityController extends StateController { mContext.unregisterReceiver(mBroadcastReceiver); synchronized (mSatLock) { + mCarrierPrivilegedApps.clear(); mPowerAllowlistedApps.clear(); mSpecialApps.clear(); + + for (int i = mCarrierPrivilegedCallbacks.size() - 1; i >= 0; --i) { + mTelephonyManager.unregisterCarrierPrivilegesCallback( + mCarrierPrivilegedCallbacks.valueAt(i)); + } + mCarrierPrivilegedCallbacks.clear(); + } + } + + private void updateCarrierPrivilegedCallbackRegistration() { + if (mTelephonyManager == null) { + return; + } + if (!mHasFeatureTelephonySubscription) { + return; + } + + Collection<UiccSlotMapping> simSlotMapping = mTelephonyManager.getSimSlotMapping(); + final ArraySet<String> changedPkgs = new ArraySet<>(); + synchronized (mSatLock) { + final IntArray callbacksToRemove = new IntArray(); + for (int i = mCarrierPrivilegedCallbacks.size() - 1; i >= 0; --i) { + callbacksToRemove.add(mCarrierPrivilegedCallbacks.keyAt(i)); + } + for (UiccSlotMapping mapping : simSlotMapping) { + final int logicalIndex = mapping.getLogicalSlotIndex(); + if (mCarrierPrivilegedCallbacks.contains(logicalIndex)) { + // Callback already exists. No need to create a new one or remove it. + callbacksToRemove.remove(logicalIndex); + continue; + } + final LogicalIndexCarrierPrivilegesCallback callback = + new LogicalIndexCarrierPrivilegesCallback(logicalIndex); + mCarrierPrivilegedCallbacks.put(logicalIndex, callback); + // Upon registration, the callbacks will be called with the current list of + // apps, so there's no need to query the app list synchronously. + mTelephonyManager.registerCarrierPrivilegesCallback(logicalIndex, + AppSchedulingModuleThread.getExecutor(), callback); + } + + for (int i = callbacksToRemove.size() - 1; i >= 0; --i) { + final int logicalIndex = callbacksToRemove.get(i); + final LogicalIndexCarrierPrivilegesCallback callback = + mCarrierPrivilegedCallbacks.get(logicalIndex); + mTelephonyManager.unregisterCarrierPrivilegesCallback(callback); + mCarrierPrivilegedCallbacks.remove(logicalIndex); + changedPkgs.addAll(mCarrierPrivilegedApps.get(logicalIndex)); + mCarrierPrivilegedApps.remove(logicalIndex); + } } + + updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs); } /** @@ -1762,18 +1856,65 @@ public final class FlexibilityController extends StateController { updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs); } + class LogicalIndexCarrierPrivilegesCallback implements + TelephonyManager.CarrierPrivilegesCallback { + public final int logicalIndex; + + LogicalIndexCarrierPrivilegesCallback(int logicalIndex) { + this.logicalIndex = logicalIndex; + } + + @Override + public void onCarrierPrivilegesChanged(@NonNull Set<String> privilegedPackageNames, + @NonNull Set<Integer> privilegedUids) { + final ArraySet<String> changedPkgs = new ArraySet<>(); + synchronized (mSatLock) { + final ArraySet<String> oldPrivilegedSet = + mCarrierPrivilegedApps.get(logicalIndex); + if (oldPrivilegedSet != null) { + changedPkgs.addAll(oldPrivilegedSet); + mCarrierPrivilegedApps.remove(logicalIndex); + } + for (String pkgName : privilegedPackageNames) { + mCarrierPrivilegedApps.add(logicalIndex, pkgName); + if (!changedPkgs.remove(pkgName)) { + // The package wasn't in the previous set of privileged apps. Add it + // since its state has changed. + changedPkgs.add(pkgName); + } + } + } + + // The carrier privileged list doesn't provide a simple userId correlation, + // so for now, use USER_ALL for these packages. + // TODO(141645789): use the UID list to narrow down to specific userIds + updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs); + } + } + public void dump(@NonNull IndentingPrintWriter pw) { pw.println("Special apps:"); pw.increaseIndent(); synchronized (mSatLock) { for (int u = 0; u < mSpecialApps.size(); ++u) { + pw.print("User "); pw.print(mSpecialApps.keyAt(u)); pw.print(": "); pw.println(mSpecialApps.valuesAt(u)); } pw.println(); + pw.println("Carrier privileged packages:"); + pw.increaseIndent(); + for (int i = 0; i < mCarrierPrivilegedApps.size(); ++i) { + pw.print(mCarrierPrivilegedApps.keyAt(i)); + pw.print(": "); + pw.println(mCarrierPrivilegedApps.valuesAt(i)); + } + pw.decreaseIndent(); + + pw.println(); pw.print("Power allowlisted packages: "); pw.println(mPowerAllowlistedApps); } diff --git a/core/api/current.txt b/core/api/current.txt index 62980ed5bd69..4c1b27db3209 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5395,7 +5395,7 @@ package android.app { public final class AutomaticZenRule implements android.os.Parcelable { 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 @Deprecated 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 public int describeContents(); method public android.net.Uri getConditionId(); @@ -6085,7 +6085,7 @@ package android.app { public class GrammaticalInflectionManager { method public int getApplicationGrammaticalGender(); - method @FlaggedApi("android.app.system_terms_of_address_enabled") public int getSystemGrammaticalGender(); + method @FlaggedApi("android.app.system_terms_of_address_enabled") @RequiresPermission("android.permission.READ_SYSTEM_GRAMMATICAL_GENDER") public int getSystemGrammaticalGender(); method public void setRequestedApplicationGrammaticalGender(int); } @@ -16361,7 +16361,7 @@ package android.graphics { ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(); ctor @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public Matrix44(@NonNull android.graphics.Matrix); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 concat(@NonNull android.graphics.Matrix44); - method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(int, int); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public float get(@IntRange(from=0, to=3) int, @IntRange(from=0, to=3) int); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void getValues(@NonNull float[]); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean invert(); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public boolean isIdentity(); @@ -16370,7 +16370,7 @@ package android.graphics { method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void reset(); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 rotate(float, float, float, float); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 scale(float, float, float); - method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(int, int, float); + method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void set(@IntRange(from=0, to=3) int, @IntRange(from=0, to=3) int, float); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") public void setValues(@NonNull float[]); method @FlaggedApi("com.android.graphics.hwui.flags.matrix_44") @NonNull public android.graphics.Matrix44 translate(float, float, float); } @@ -41153,19 +41153,19 @@ package android.service.notification { method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications(); method public final void migrateNotificationFilter(int, @Nullable java.util.List<java.lang.String>); method public android.os.IBinder onBind(android.content.Intent); - method public void onInterruptionFilterChanged(int); - method public void onListenerConnected(); - method public void onListenerDisconnected(); - method public void onListenerHintsChanged(int); - method public void onNotificationChannelGroupModified(String, android.os.UserHandle, android.app.NotificationChannelGroup, int); - method public void onNotificationChannelModified(String, android.os.UserHandle, android.app.NotificationChannel, int); - method public void onNotificationPosted(android.service.notification.StatusBarNotification); - method public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap); - method public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap); - method public void onNotificationRemoved(android.service.notification.StatusBarNotification); - method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap); - method public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, int); - method public void onSilentStatusBarIconsVisibilityChanged(boolean); + method @UiThread public void onInterruptionFilterChanged(int); + method @UiThread public void onListenerConnected(); + method @UiThread public void onListenerDisconnected(); + method @UiThread public void onListenerHintsChanged(int); + method @UiThread public void onNotificationChannelGroupModified(String, android.os.UserHandle, android.app.NotificationChannelGroup, int); + method @UiThread public void onNotificationChannelModified(String, android.os.UserHandle, android.app.NotificationChannel, int); + method @UiThread public void onNotificationPosted(android.service.notification.StatusBarNotification); + method @UiThread public void onNotificationPosted(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap); + method @UiThread public void onNotificationRankingUpdate(android.service.notification.NotificationListenerService.RankingMap); + method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification); + method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap); + method @UiThread public void onNotificationRemoved(android.service.notification.StatusBarNotification, android.service.notification.NotificationListenerService.RankingMap, int); + method @UiThread public void onSilentStatusBarIconsVisibilityChanged(boolean); method public final void requestInterruptionFilter(int); method public final void requestListenerHints(int); method public static void requestRebind(android.content.ComponentName); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 9c1a8e854e92..0ab2588dd87e 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -652,6 +652,7 @@ package android.webkit { method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.webkit.WebViewProviderResponse> CREATOR; field public static final int STATUS_FAILED_LISTING_WEBVIEW_PACKAGES = 4; // 0x4 + field public static final int STATUS_FAILED_OTHER = 11; // 0xb field public static final int STATUS_FAILED_WAITING_FOR_RELRO = 3; // 0x3 field public static final int STATUS_SUCCESS = 0; // 0x0 field @Nullable public final android.content.pm.PackageInfo packageInfo; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index afb796b7687a..adc7ef1990a5 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4386,7 +4386,7 @@ package android.content.pm { field public static final int PROTECTION_FLAG_MODULE = 4194304; // 0x400000 field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000 field public static final int PROTECTION_FLAG_RECENTS = 33554432; // 0x2000000 - field public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000 + field @Deprecated public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000 field public static final int PROTECTION_FLAG_ROLE = 67108864; // 0x4000000 field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000 field public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 32768; // 0x8000 @@ -4821,7 +4821,7 @@ package android.hardware { method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener); method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public void addSensorPrivacyListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener); method @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean areAnySensorPrivacyTogglesEnabled(int); - method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @NonNull @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public java.util.Map<java.lang.String,java.lang.Boolean> getCameraPrivacyAllowlist(); + method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @NonNull @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public java.util.List<java.lang.String> getCameraPrivacyAllowlist(); method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public int getSensorPrivacyState(int, int); method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isCameraPrivacyEnabled(@NonNull String); method @Deprecated @RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY) public boolean isSensorPrivacyEnabled(int); @@ -4844,6 +4844,12 @@ package android.hardware { method public boolean isEnabled(); } + @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") public static class SensorPrivacyManager.StateTypes { + field public static final int DISABLED = 2; // 0x2 + field public static final int ENABLED = 1; // 0x1 + field public static final int ENABLED_EXCEPT_ALLOWLISTED_APPS = 3; // 0x3 + } + } package android.hardware.biometrics { @@ -12885,7 +12891,7 @@ package android.service.notification { } public abstract class NotificationListenerService extends android.app.Service { - method public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int); + method @UiThread public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int); } public static class NotificationListenerService.Ranking { @@ -12967,7 +12973,7 @@ package android.service.ondeviceintelligence { field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceIntelligenceService"; } - public static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception { + public abstract static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception { ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int); ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int, @NonNull String); method public int getErrorCode(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 6fb6b0d1d5bf..53d0c032dbaf 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1519,6 +1519,7 @@ package android.graphics.fonts { package android.hardware { public final class SensorPrivacyManager { + method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setCameraPrivacyAllowlist(@NonNull java.util.List<java.lang.String>); method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, int, boolean); method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacyState(int, int, int); } diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index f6ec370478a9..5e2397d41424 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -173,8 +173,8 @@ public final class AutomaticZenRule implements Parcelable { * interrupt the user (e.g. via sound & vibration) while this rule * is active. * @param enabled Whether the rule is enabled. - * @deprecated use {@link #AutomaticZenRule(String, ComponentName, ComponentName, Uri, - * ZenPolicy, int, boolean)}. + * + * @deprecated Use {@link AutomaticZenRule.Builder} to construct an {@link AutomaticZenRule}. */ @Deprecated public AutomaticZenRule(String name, ComponentName owner, Uri conditionId, @@ -206,8 +206,10 @@ public final class AutomaticZenRule implements Parcelable { * while this rule is active. This overrides the global policy while this rule is * action ({@link Condition#STATE_TRUE}). * @param enabled Whether the rule is enabled. + * + * @deprecated Use {@link AutomaticZenRule.Builder} to construct an {@link AutomaticZenRule}. */ - // TODO (b/309088420): deprecate this constructor in favor of the builder + @Deprecated public AutomaticZenRule(@NonNull String name, @Nullable ComponentName owner, @Nullable ComponentName configurationActivity, @NonNull Uri conditionId, @Nullable ZenPolicy policy, int interruptionFilter, boolean enabled) { @@ -368,6 +370,9 @@ public final class AutomaticZenRule implements Parcelable { /** * Sets the zen policy. + * + * <p>When updating an existing rule via {@link NotificationManager#updateAutomaticZenRule}, + * a {@code null} value here means the previous policy is retained. */ public void setZenPolicy(@Nullable ZenPolicy zenPolicy) { this.mZenPolicy = (zenPolicy == null ? null : zenPolicy.copy()); @@ -390,7 +395,12 @@ public final class AutomaticZenRule implements Parcelable { * Sets the configuration activity - an activity that handles * {@link NotificationManager#ACTION_AUTOMATIC_ZEN_RULE} that shows the user more information * about this rule and/or allows them to configure it. This is required to be non-null for rules - * that are not backed by {@link android.service.notification.ConditionProviderService}. + * that are not backed by a {@link android.service.notification.ConditionProviderService}. + * + * <p>This is exclusive with the {@code owner} supplied in the constructor; rules where a + * configuration activity is set will not use the + * {@link android.service.notification.ConditionProviderService} supplied there to determine + * whether the rule should be active. */ public void setConfigurationActivity(@Nullable ComponentName componentName) { this.configurationActivity = getTrimmedComponentName(componentName); diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java index 483a6e11a42a..4ce983f6019b 100644 --- a/core/java/android/app/GrammaticalInflectionManager.java +++ b/core/java/android/app/GrammaticalInflectionManager.java @@ -16,8 +16,10 @@ package android.app; +import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.content.Context; import android.content.res.Configuration; @@ -127,6 +129,7 @@ public class GrammaticalInflectionManager { * * @see Configuration#getGrammaticalGender */ + @RequiresPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER) @FlaggedApi(Flags.FLAG_SYSTEM_TERMS_OF_ADDRESS_ENABLED) @Configuration.GrammaticalGender public int getSystemGrammaticalGender() { diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index d49a2542eed8..b82a1e3d8dfa 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -270,13 +270,16 @@ public class NotificationManager { * Integer extra for {@link #ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED} containing the state of * the {@link AutomaticZenRule}. * - * <p> - * The value will be one of {@link #AUTOMATIC_RULE_STATUS_ENABLED}, - * {@link #AUTOMATIC_RULE_STATUS_DISABLED}, {@link #AUTOMATIC_RULE_STATUS_REMOVED}, - * {@link #AUTOMATIC_RULE_STATUS_UNKNOWN}. - * </p> + * <p>The value will be one of {@link #AUTOMATIC_RULE_STATUS_ENABLED}, + * {@link #AUTOMATIC_RULE_STATUS_DISABLED}, {@link #AUTOMATIC_RULE_STATUS_REMOVED}, + * {@link #AUTOMATIC_RULE_STATUS_ACTIVATED}, {@link #AUTOMATIC_RULE_STATUS_DEACTIVATED}, or + * {@link #AUTOMATIC_RULE_STATUS_UNKNOWN}. + * + * <p>Note that the {@link #AUTOMATIC_RULE_STATUS_ACTIVATED} and + * {@link #AUTOMATIC_RULE_STATUS_DEACTIVATED} statuses are only sent to packages targeting + * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above; apps targeting a lower SDK version + * will be sent {@link #AUTOMATIC_RULE_STATUS_UNKNOWN} in their place instead. */ - // TODO (b/309101513): Add new status types to javadoc public static final String EXTRA_AUTOMATIC_ZEN_RULE_STATUS = "android.app.extra.AUTOMATIC_ZEN_RULE_STATUS"; @@ -370,11 +373,15 @@ public class NotificationManager { = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED"; /** - * Intent that is broadcast when the state of getNotificationPolicy() changes. + * Intent that is broadcast when the state of {@link #getNotificationPolicy()} changes. * * <p>This broadcast is only sent to registered receivers and (starting from * {@link Build.VERSION_CODES#Q}) receivers in packages that have been granted Do Not * Disturb access (see {@link #isNotificationPolicyAccessGranted()}). + * + * <p>Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, most calls to + * {@link #setNotificationPolicy(Policy)} will update the app's implicit rule policy instead of + * the global policy, so this broadcast will be sent much less frequently. */ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_NOTIFICATION_POLICY_CHANGED @@ -1378,12 +1385,16 @@ public class NotificationManager { /** * Updates the given zen rule. * - * <p> - * Throws a SecurityException if policy access is not granted to this package. + * <p>Before {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, updating a rule that is not backed + * up by a {@link android.service.notification.ConditionProviderService} will deactivate it if + * it was previously active. Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this + * will only happen if the rule's definition is actually changing. + * + * <p>Throws a SecurityException if policy access is not granted to this package. * See {@link #isNotificationPolicyAccessGranted}. * - * <p> - * Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}. + * <p>Callers can only update rules that they own. See {@link AutomaticZenRule#getOwner}. + * * @param id The id of the rule to update * @param automaticZenRule the rule to update. * @return Whether the rule was successfully updated. @@ -1744,9 +1755,11 @@ public class NotificationManager { /** * Gets the current user-specified default notification policy. * - * <p> + * <p>For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some + * exceptions, such as companion device managers) this method will return the policy associated + * to their implicit {@link AutomaticZenRule} instead, if it exists. See + * {@link #setNotificationPolicy(Policy)}. */ - // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public Policy getNotificationPolicy() { INotificationManager service = getService(); try { @@ -1757,15 +1770,20 @@ public class NotificationManager { } /** - * Sets the current notification policy. + * Sets the current notification policy (which applies when {@link #setInterruptionFilter} is + * called with the {@link #INTERRUPTION_FILTER_PRIORITY} value). * - * <p> - * Only available if policy access is granted to this package. - * See {@link #isNotificationPolicyAccessGranted}. + * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some + * exceptions, such as companion device managers) cannot modify the global notification policy. + * Calling this method will instead create or update an {@link AutomaticZenRule} associated to + * the app, using a {@link ZenPolicy} corresponding to the {@link Policy} supplied here, and + * which will be activated/deactivated by calls to {@link #setInterruptionFilter(int)}. + * + * <p>Only available if policy access is granted to this package. See + * {@link #isNotificationPolicyAccessGranted}. * * @param policy The new desired policy. */ - // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public void setNotificationPolicy(@NonNull Policy policy) { setNotificationPolicy(policy, /* fromUser= */ false); } @@ -2052,10 +2070,12 @@ public class NotificationManager { /** Notification senders to prioritize for calls. One of: * PRIORITY_SENDERS_ANY, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED */ + @PrioritySenders public final int priorityCallSenders; /** Notification senders to prioritize for messages. One of: * PRIORITY_SENDERS_ANY, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED */ + @PrioritySenders public final int priorityMessageSenders; /** @@ -2063,6 +2083,7 @@ public class NotificationManager { * {@link #CONVERSATION_SENDERS_NONE}, {@link #CONVERSATION_SENDERS_IMPORTANT}, * {@link #CONVERSATION_SENDERS_ANYONE}. */ + @ConversationSenders public final int priorityConversationSenders; /** @@ -2630,16 +2651,19 @@ public class NotificationManager { } /** @hide **/ + @PrioritySenders public int allowCallsFrom() { return priorityCallSenders; } /** @hide **/ + @PrioritySenders public int allowMessagesFrom() { return priorityMessageSenders; } /** @hide **/ + @ConversationSenders public int allowConversationsFrom() { return priorityConversationSenders; } @@ -2780,11 +2804,17 @@ public class NotificationManager { * The interruption filter defines which notifications are allowed to * interrupt the user (e.g. via sound & vibration) and is applied * globally. - * <p> - * Only available if policy access is granted to this package. See + * + * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some + * exceptions, such as companion device managers) cannot modify the global interruption filter. + * Calling this method will instead activate or deactivate an {@link AutomaticZenRule} + * associated to the app, using a {@link ZenPolicy} that corresponds to the {@link Policy} + * supplied to {@link #setNotificationPolicy(Policy)} (or the global policy when one wasn't + * provided). + * + * <p> Only available if policy access is granted to this package. See * {@link #isNotificationPolicyAccessGranted}. */ - // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior. public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) { setInterruptionFilter(interruptionFilter, /* fromUser= */ false); } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 625526047212..8b84f062b7b5 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -124,6 +124,32 @@ public class ResourcesManager { */ private LocaleConfig mLocaleConfig = new LocaleConfig(LocaleList.getEmptyLocaleList()); + private final ArrayMap<String, SharedLibraryAssets> mSharedLibAssetsMap = + new ArrayMap<>(); + + /** + * The internal function to register the resources paths of a package (e.g. a shared library). + * This will collect the package resources' paths from its ApplicationInfo and add them to all + * existing and future contexts while the application is running. + */ + public void registerResourcePaths(@NonNull String uniqueId, @NonNull ApplicationInfo appInfo) { + SharedLibraryAssets sharedLibAssets = new SharedLibraryAssets(appInfo.sourceDir, + appInfo.splitSourceDirs, appInfo.sharedLibraryFiles, + appInfo.resourceDirs, appInfo.overlayPaths); + + synchronized (mLock) { + if (mSharedLibAssetsMap.containsKey(uniqueId)) { + Slog.v(TAG, "Package resources' paths for uniqueId: " + uniqueId + + " has already been registered, this is a no-op."); + return; + } + mSharedLibAssetsMap.put(uniqueId, sharedLibAssets); + appendLibAssetsLocked(sharedLibAssets.getAllAssetPaths()); + Slog.v(TAG, "The following resources' paths have been added: " + + Arrays.toString(sharedLibAssets.getAllAssetPaths())); + } + } + private static class ApkKey { public final String path; public final boolean sharedLib; @@ -278,6 +304,21 @@ public class ResourcesManager { public ResourcesManager() { } + /** + * Inject a customized ResourcesManager instance for testing, return the old ResourcesManager + * instance. + */ + @UnsupportedAppUsage + @VisibleForTesting + public static ResourcesManager setInstance(ResourcesManager resourcesManager) { + synchronized (ResourcesManager.class) { + ResourcesManager oldResourceManager = sResourcesManager; + sResourcesManager = resourcesManager; + return oldResourceManager; + } + + } + @UnsupportedAppUsage public static ResourcesManager getInstance() { synchronized (ResourcesManager.class) { @@ -1480,6 +1521,56 @@ public class ResourcesManager { } } + private void appendLibAssetsLocked(String[] libAssets) { + synchronized (mLock) { + // Record which ResourcesImpl need updating + // (and what ResourcesKey they should update to). + final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>(); + + final int implCount = mResourceImpls.size(); + for (int i = 0; i < implCount; i++) { + final ResourcesKey key = mResourceImpls.keyAt(i); + final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i); + final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; + if (impl == null) { + Slog.w(TAG, "Found a ResourcesImpl which is null, skip it and continue to " + + "append shared library assets for next ResourcesImpl."); + continue; + } + + var newDirs = new ArrayList<String>(); + var dirsSet = new ArraySet<String>(); + if (key.mLibDirs != null) { + final int dirsLength = key.mLibDirs.length; + for (int k = 0; k < dirsLength; k++) { + newDirs.add(key.mLibDirs[k]); + dirsSet.add(key.mLibDirs[k]); + } + } + final int assetsLength = libAssets.length; + for (int j = 0; j < assetsLength; j++) { + if (dirsSet.add(libAssets[j])) { + newDirs.add(libAssets[j]); + } + } + String[] newLibAssets = newDirs.toArray(new String[0]); + if (!Arrays.equals(newLibAssets, key.mLibDirs)) { + updatedResourceKeys.put(impl, new ResourcesKey( + key.mResDir, + key.mSplitResDirs, + key.mOverlayPaths, + newLibAssets, + key.mDisplayId, + key.mOverrideConfiguration, + key.mCompatInfo, + key.mLoaders)); + } + } + + redirectResourcesToNewImplLocked(updatedResourceKeys); + } + } + private void applyNewResourceDirsLocked(@Nullable final String[] oldSourceDirs, @NonNull final ApplicationInfo appInfo) { try { @@ -1689,4 +1780,50 @@ public class ResourcesManager { } } } + + public static class SharedLibraryAssets{ + private final String[] mAssetPaths; + + SharedLibraryAssets(String sourceDir, String[] splitSourceDirs, String[] sharedLibraryFiles, + String[] resourceDirs, String[] overlayPaths) { + mAssetPaths = collectAssetPaths(sourceDir, splitSourceDirs, sharedLibraryFiles, + resourceDirs, overlayPaths); + } + + private @NonNull String[] collectAssetPaths(String sourceDir, String[] splitSourceDirs, + String[] sharedLibraryFiles, String[] resourceDirs, String[] overlayPaths) { + final String[][] inputLists = { + splitSourceDirs, sharedLibraryFiles, resourceDirs, overlayPaths + }; + + final ArraySet<String> assetPathSet = new ArraySet<>(); + final List<String> assetPathList = new ArrayList<>(); + if (sourceDir != null) { + assetPathSet.add(sourceDir); + assetPathList.add(sourceDir); + } + + for (int i = 0; i < inputLists.length; i++) { + if (inputLists[i] != null) { + for (int j = 0; j < inputLists[i].length; j++) { + if (assetPathSet.add(inputLists[i][j])) { + assetPathList.add(inputLists[i][j]); + } + } + } + } + return assetPathList.toArray(new String[0]); + } + + /** + * @return all the asset paths of this collected in this class. + */ + public @NonNull String[] getAllAssetPaths() { + return mAssetPaths; + } + } + + public @NonNull ArrayMap<String, SharedLibraryAssets> getSharedLibAssetsMap() { + return new ArrayMap<>(mSharedLibAssetsMap); + } } diff --git a/core/java/android/app/admin/BundlePolicyValue.java b/core/java/android/app/admin/BundlePolicyValue.java index 4f7060461091..cb5e9861141d 100644 --- a/core/java/android/app/admin/BundlePolicyValue.java +++ b/core/java/android/app/admin/BundlePolicyValue.java @@ -32,7 +32,7 @@ public final class BundlePolicyValue extends PolicyValue<Bundle> { public BundlePolicyValue(Bundle value) { super(value); if (Flags.devicePolicySizeTrackingInternalEnabled()) { - PolicySizeVerifier.enforceMaxParcelableFieldsLength(value); + PolicySizeVerifier.enforceMaxBundleFieldsLength(value); } } diff --git a/core/java/android/app/admin/IntentFilterPolicyKey.java b/core/java/android/app/admin/IntentFilterPolicyKey.java index 63c3a4cb499f..7526a7b2c934 100644 --- a/core/java/android/app/admin/IntentFilterPolicyKey.java +++ b/core/java/android/app/admin/IntentFilterPolicyKey.java @@ -24,7 +24,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.admin.flags.Flags; import android.content.IntentFilter; import android.os.Bundle; import android.os.Parcel; @@ -60,9 +59,6 @@ public final class IntentFilterPolicyKey extends PolicyKey { @TestApi public IntentFilterPolicyKey(@NonNull String identifier, @NonNull IntentFilter filter) { super(identifier); - if (Flags.devicePolicySizeTrackingInternalEnabled()) { - PolicySizeVerifier.enforceMaxParcelableFieldsLength(filter); - } mFilter = Objects.requireNonNull(filter); } diff --git a/core/java/android/app/admin/PolicySizeVerifier.java b/core/java/android/app/admin/PolicySizeVerifier.java index 792ebc6ad297..7f8e50ec4420 100644 --- a/core/java/android/app/admin/PolicySizeVerifier.java +++ b/core/java/android/app/admin/PolicySizeVerifier.java @@ -17,12 +17,12 @@ package android.app.admin; import android.content.ComponentName; +import android.os.Bundle; import android.os.Parcelable; import android.os.PersistableBundle; import com.android.internal.util.Preconditions; -import java.lang.reflect.Field; import java.util.ArrayDeque; import java.util.Queue; @@ -71,44 +71,51 @@ public class PolicySizeVerifier { for (String key : current.keySet()) { enforceMaxStringLength(key, "key in " + argName); Object value = current.get(key); - if (value instanceof String) { - enforceMaxStringLength((String) value, "string value in " + argName); - } else if (value instanceof String[]) { - for (String str : (String[]) value) { + if (value instanceof String str) { + enforceMaxStringLength(str, "string value in " + argName); + } else if (value instanceof String[] strArray) { + for (String str : strArray) { enforceMaxStringLength(str, "string value in " + argName); } - } else if (value instanceof PersistableBundle) { - queue.add((PersistableBundle) value); + } else if (value instanceof PersistableBundle persistableBundle) { + queue.add(persistableBundle); } } } } /** - * Throw if Parcelable contains any string that's too long to be serialized. + * Throw if bundle contains any string that's too long to be serialized. This follows the + * serialization logic in BundlePolicySerializer#writeBundle. */ - public static void enforceMaxParcelableFieldsLength(Parcelable parcelable) { - // TODO(b/326662716) rework to protect against infinite recursion. - if (true) { - return; - } - Class<?> clazz = parcelable.getClass(); - - Field[] fields = clazz.getDeclaredFields(); - for (Field field : fields) { - field.setAccessible(true); - try { - Object value = field.get(parcelable); - if (value instanceof String) { - String stringValue = (String) value; - enforceMaxStringLength(stringValue, field.getName()); + public static void enforceMaxBundleFieldsLength(Bundle bundle) { + Queue<Bundle> queue = new ArrayDeque<>(); + queue.add(bundle); + while (!queue.isEmpty()) { + Bundle current = queue.remove(); + for (String key : current.keySet()) { + enforceMaxStringLength(key, "key in Bundle"); + Object value = current.get(key); + if (value instanceof String str) { + enforceMaxStringLength(str, "string value in Bundle with " + + "key" + key); + } else if (value instanceof String[] strArray) { + for (String str : strArray) { + enforceMaxStringLength(str, "string value in Bundle with" + + " key" + key); + } + } else if (value instanceof Bundle b) { + queue.add(b); } - - if (value instanceof Parcelable) { - enforceMaxParcelableFieldsLength((Parcelable) value); + else if (value instanceof Parcelable[] parcelableArray) { + for (Parcelable parcelable : parcelableArray) { + if (!(parcelable instanceof Bundle)) { + throw new IllegalArgumentException("bundle-array can only hold " + + "Bundles"); + } + queue.add((Bundle) parcelable); + } } - } catch (IllegalAccessException e) { - e.printStackTrace(); } } } diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java index cdda12eebdc4..3f941dad2c3f 100644 --- a/core/java/android/content/pm/PermissionInfo.java +++ b/core/java/android/content/pm/PermissionInfo.java @@ -273,6 +273,10 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { * to the <code>retailDemo</code> value of * {@link android.R.attr#protectionLevel}. * + * @deprecated This flag has been replaced by the + * {@link android.R.string#config_defaultRetailDemo retail demo role} and is a + * no-op since {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}. + * * @hide */ @SystemApi diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index d259e9755a41..273e40a21bb2 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -471,6 +471,16 @@ public final class AssetManager implements AutoCloseable { return addAssetPathInternal(path, true /*overlay*/, false /*appAsLib*/); } + /** + * @hide + */ + public void addSharedLibraryPaths(@NonNull String[] paths) { + final int length = paths.length; + for (int i = 0; i < length; i++) { + addAssetPathInternal(paths[i], false, true); + } + } + private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) { Objects.requireNonNull(path, "path"); synchronized (this) { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 7fba3e890ec6..1f5f88f51d55 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -43,6 +43,7 @@ import android.annotation.StyleRes; import android.annotation.StyleableRes; import android.annotation.XmlRes; import android.app.Application; +import android.app.ResourcesManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.ActivityInfo; @@ -2854,6 +2855,11 @@ public class Resources { @FlaggedApi(android.content.res.Flags.FLAG_REGISTER_RESOURCE_PATHS) public static void registerResourcePaths(@NonNull String uniqueId, @NonNull ApplicationInfo appInfo) { - throw new UnsupportedOperationException("The implementation has not been done yet."); + if (Flags.registerResourcePaths()) { + ResourcesManager.getInstance().registerResourcePaths(uniqueId, appInfo); + } else { + throw new UnsupportedOperationException("Flag " + Flags.FLAG_REGISTER_RESOURCE_PATHS + + " is disabled."); + } } } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 079c2c1ab7c9..8d045aaf4d81 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -29,6 +29,7 @@ import android.annotation.StyleRes; import android.annotation.StyleableRes; import android.app.LocaleConfig; import android.app.ResourcesManager; +import android.app.ResourcesManager.SharedLibraryAssets; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; @@ -47,6 +48,7 @@ import android.os.Build; import android.os.LocaleList; import android.os.ParcelFileDescriptor; import android.os.Trace; +import android.util.ArrayMap; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -197,6 +199,14 @@ public class ResourcesImpl { public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics, @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) { mAssets = assets; + if (Flags.registerResourcePaths()) { + ArrayMap<String, SharedLibraryAssets> sharedLibMap = + ResourcesManager.getInstance().getSharedLibAssetsMap(); + final int size = sharedLibMap.size(); + for (int i = 0; i < size; i++) { + assets.addSharedLibraryPaths(sharedLibMap.valueAt(i).getAllAssetPaths()); + } + } mMetrics.setToDefaults(); mDisplayAdjustments = displayAdjustments; mConfiguration.setToDefaults(); diff --git a/core/java/android/hardware/ISensorPrivacyManager.aidl b/core/java/android/hardware/ISensorPrivacyManager.aidl index 851ce2add94f..19d10294a75a 100644 --- a/core/java/android/hardware/ISensorPrivacyManager.aidl +++ b/core/java/android/hardware/ISensorPrivacyManager.aidl @@ -16,7 +16,6 @@ package android.hardware; -import android.hardware.CameraPrivacyAllowlistEntry; import android.hardware.ISensorPrivacyListener; /** @hide */ @@ -48,7 +47,7 @@ interface ISensorPrivacyManager { void setToggleSensorPrivacyForProfileGroup(int userId, int source, int sensor, boolean enable); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)") - List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist(); + List<String> getCameraPrivacyAllowlist(); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)") int getToggleSensorPrivacyState(int toggleType, int sensor); @@ -62,6 +61,10 @@ interface ISensorPrivacyManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY)") boolean isCameraPrivacyEnabled(String packageName); + /** @hide */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY)") + void setCameraPrivacyAllowlist(in List<String> allowlist); + // =============== End of transactions used on native side as well ============================ void suppressToggleSensorPrivacyReminders(int userId, int sensor, IBinder token, diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java index 6294a8d617de..08b906439b08 100644 --- a/core/java/android/hardware/SensorPrivacyManager.java +++ b/core/java/android/hardware/SensorPrivacyManager.java @@ -43,7 +43,7 @@ import com.android.internal.camera.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Map; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -204,6 +204,8 @@ public final class SensorPrivacyManager { * Types of state which can exist for the sensor privacy toggle * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) public static class StateTypes { private StateTypes() {} @@ -217,30 +219,12 @@ public final class SensorPrivacyManager { */ public static final int DISABLED = SensorPrivacyIndividualEnabledSensorProto.DISABLED; - /** - * Constant indicating privacy is enabled except for the automotive driver assistance apps - * which are helpful for driving. - */ - @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS = - SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS; - /** * Constant indicating privacy is enabled except for the automotive driver assistance apps * which are required by car manufacturer for driving. */ - @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS = - SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS; - - /** - * Constant indicating privacy is enabled except for the automotive driver assistance apps - * which are both helpful for driving and also apps required by car manufacturer for - * driving. - */ - @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - public static final int AUTOMOTIVE_DRIVER_ASSISTANCE_APPS = - SensorPrivacyIndividualEnabledSensorProto.AUTO_DRIVER_ASSISTANCE_APPS; + public static final int ENABLED_EXCEPT_ALLOWLISTED_APPS = + SensorPrivacyIndividualEnabledSensorProto.ENABLED_EXCEPT_ALLOWLISTED_APPS; /** * Types of state which can exist for a sensor privacy toggle @@ -250,9 +234,7 @@ public final class SensorPrivacyManager { @IntDef(value = { ENABLED, DISABLED, - AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS, - AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS, - AUTOMOTIVE_DRIVER_ASSISTANCE_APPS + ENABLED_EXCEPT_ALLOWLISTED_APPS }) @Retention(RetentionPolicy.SOURCE) public @interface StateType {} @@ -369,9 +351,6 @@ public final class SensorPrivacyManager { private final ArrayMap<Pair<Integer, OnSensorPrivacyChangedListener>, OnSensorPrivacyChangedListener> mLegacyToggleListeners = new ArrayMap<>(); - @GuardedBy("mLock") - private ArrayMap<String, Boolean> mCameraPrivacyAllowlist = null; - /** The singleton ISensorPrivacyListener for IPC which will be used to dispatch to local * listeners */ @NonNull @@ -397,7 +376,8 @@ public final class SensorPrivacyManager { @Override @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - public void onSensorPrivacyStateChanged(int toggleType, int sensor, int state) { + public void onSensorPrivacyStateChanged(@ToggleType int toggleType, + @Sensors.Sensor int sensor, @StateTypes.StateType int state) { synchronized (mLock) { for (int i = 0; i < mToggleListeners.size(); i++) { OnSensorPrivacyChangedListener listener = mToggleListeners.keyAt(i); @@ -725,6 +705,8 @@ public final class SensorPrivacyManager { /** * Returns sensor privacy state for a specific sensor. * + * @param toggleType The type of toggle to use + * @param sensor The sensor to check * @return int sensor privacy state. * * @hide @@ -741,9 +723,10 @@ public final class SensorPrivacyManager { } } - /** + /** * Returns if camera privacy is enabled for a specific package. * + * @param packageName The package to check * @return boolean sensor privacy state. * * @hide @@ -763,29 +746,41 @@ public final class SensorPrivacyManager { * Returns camera privacy allowlist. * * @return List of automotive driver assistance packages for - * privacy allowlisting. The returned map includes the package - * name as key and the value is a Boolean which tells if that package - * is required by the car manufacturer as mandatory package for driving. + * privacy allowlisting. * * @hide */ @SystemApi @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY) @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - public @NonNull Map<String, Boolean> getCameraPrivacyAllowlist() { + public @NonNull List<String> getCameraPrivacyAllowlist() { synchronized (mLock) { - if (mCameraPrivacyAllowlist == null) { - mCameraPrivacyAllowlist = new ArrayMap<>(); - try { - for (CameraPrivacyAllowlistEntry entry : - mService.getCameraPrivacyAllowlist()) { - mCameraPrivacyAllowlist.put(entry.packageName, entry.isMandatory); - } - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + try { + return mService.getCameraPrivacyAllowlist(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Sets camera privacy allowlist. + * + * @param allowlist List of automotive driver assistance packages for + * privacy allowlisting. + * + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.MANAGE_SENSOR_PRIVACY) + @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) + public void setCameraPrivacyAllowlist(@NonNull List<String> allowlist) { + synchronized (mLock) { + try { + mService.setCameraPrivacyAllowlist(allowlist); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return mCameraPrivacyAllowlist; } } @@ -867,6 +862,7 @@ public final class SensorPrivacyManager { /** * Sets sensor privacy to the specified state for an individual sensor. * + * @param source the source using which the sensor is toggled * @param sensor the sensor which to change the state for * @param state the state to which sensor privacy should be set. * diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index de7008b19f54..dc782d4e1e9b 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -138,3 +138,11 @@ flag { bug: "325356776" } +flag { + name: "runtime_permission_appops_mapping_enabled" + is_fixed_read_only: true + namespace: "permissions" + description: "Use runtime permission state to determine appop state" + bug: "266164193" +} + diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index c03dc718d1a1..120846ca593b 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -667,7 +667,8 @@ public class CallLog { @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER) public @NonNull AddCallParametersBuilder setAssertedDisplayName( String assertedDisplayName) { - if (assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) { + if (assertedDisplayName != null + && assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) { throw new IllegalArgumentException("assertedDisplayName exceeds the character" + " limit of " + MAX_NUMBER_OF_CHARACTERS + "."); } diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 7658af53a7f8..bd9ab86fa8d1 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; +import android.annotation.UiThread; import android.app.ActivityManager; import android.app.INotificationManager; import android.app.Notification; @@ -468,6 +469,7 @@ public abstract class NotificationListenerService extends Service { * object as well as its identifying information (tag and id) and source * (package name). */ + @UiThread public void onNotificationPosted(StatusBarNotification sbn) { // optional } @@ -481,6 +483,7 @@ public abstract class NotificationListenerService extends Service { * @param rankingMap The current ranking map that can be used to retrieve ranking information * for active notifications, including the newly posted one. */ + @UiThread public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { onNotificationPosted(sbn); } @@ -499,6 +502,7 @@ public abstract class NotificationListenerService extends Service { * and source (package name) used to post the {@link android.app.Notification} that * was just removed. */ + @UiThread public void onNotificationRemoved(StatusBarNotification sbn) { // optional } @@ -520,6 +524,7 @@ public abstract class NotificationListenerService extends Service { * for active notifications. * */ + @UiThread public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { onNotificationRemoved(sbn); } @@ -541,6 +546,7 @@ public abstract class NotificationListenerService extends Service { * @param rankingMap The current ranking map that can be used to retrieve ranking information * for active notifications. */ + @UiThread public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, @NotificationCancelReason int reason) { onNotificationRemoved(sbn, rankingMap); @@ -552,6 +558,7 @@ public abstract class NotificationListenerService extends Service { * * @hide */ + @UiThread @SystemApi public void onNotificationRemoved(@NonNull StatusBarNotification sbn, @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason) { @@ -563,6 +570,7 @@ public abstract class NotificationListenerService extends Service { * the notification manager. You are safe to call {@link #getActiveNotifications()} * at this time. */ + @UiThread public void onListenerConnected() { // optional } @@ -572,6 +580,7 @@ public abstract class NotificationListenerService extends Service { * notification manager.You will not receive any events after this call, and may only * call {@link #requestRebind(ComponentName)} at this time. */ + @UiThread public void onListenerDisconnected() { // optional } @@ -582,6 +591,7 @@ public abstract class NotificationListenerService extends Service { * @param rankingMap The current ranking map that can be used to retrieve ranking information * for active notifications. */ + @UiThread public void onNotificationRankingUpdate(RankingMap rankingMap) { // optional } @@ -592,6 +602,7 @@ public abstract class NotificationListenerService extends Service { * * @param hints The current {@link #getCurrentListenerHints() listener hints}. */ + @UiThread public void onListenerHintsChanged(int hints) { // optional } @@ -603,6 +614,7 @@ public abstract class NotificationListenerService extends Service { * @param hideSilentStatusIcons whether or not status bar icons should be hidden for silent * notifications */ + @UiThread public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) { // optional } @@ -620,6 +632,7 @@ public abstract class NotificationListenerService extends Service { * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED}, * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}. */ + @UiThread public void onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType) { // optional @@ -638,6 +651,7 @@ public abstract class NotificationListenerService extends Service { * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED}, * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}. */ + @UiThread public void onNotificationChannelGroupModified(String pkg, UserHandle user, NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType) { // optional @@ -650,6 +664,7 @@ public abstract class NotificationListenerService extends Service { * @param interruptionFilter The current * {@link #getCurrentInterruptionFilter() interruption filter}. */ + @UiThread public void onInterruptionFilterChanged(int interruptionFilter) { // optional } @@ -1197,6 +1212,11 @@ public abstract class NotificationListenerService extends Service { * <p> * Listen for updates using {@link #onInterruptionFilterChanged(int)}. * + * <p>Apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above (with some + * exceptions, such as companion device managers) cannot modify the global interruption filter. + * Calling this method will instead activate or deactivate an + * {@link android.app.AutomaticZenRule} associated to the app. + * * <p>The service should wait for the {@link #onListenerConnected()} event * before performing this operation. * diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/core/java/android/service/notification/ZenAdapters.java index 37b263c3e3bd..b249815ce8db 100644 --- a/services/core/java/com/android/server/notification/ZenAdapters.java +++ b/core/java/android/service/notification/ZenAdapters.java @@ -14,25 +14,28 @@ * limitations under the License. */ -package com.android.server.notification; +package android.service.notification; +import android.annotation.NonNull; import android.app.Flags; import android.app.NotificationManager.Policy; -import android.service.notification.ZenModeConfig; -import android.service.notification.ZenPolicy; /** * Converters between different Zen representations. + * @hide */ -class ZenAdapters { +public class ZenAdapters { - static ZenPolicy notificationPolicyToZenPolicy(Policy policy) { + /** Maps {@link Policy} to {@link ZenPolicy}. */ + @NonNull + public static ZenPolicy notificationPolicyToZenPolicy(@NonNull Policy policy) { ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder() .allowAlarms(policy.allowAlarms()) .allowCalls( policy.allowCalls() - ? ZenModeConfig.getZenPolicySenders(policy.allowCallsFrom()) - : ZenPolicy.PEOPLE_TYPE_NONE) + ? notificationPolicySendersToZenPolicyPeopleType( + policy.allowCallsFrom()) + : ZenPolicy.PEOPLE_TYPE_NONE) .allowConversations( policy.allowConversations() ? notificationPolicyConversationSendersToZenPolicy( @@ -42,7 +45,8 @@ class ZenAdapters { .allowMedia(policy.allowMedia()) .allowMessages( policy.allowMessages() - ? ZenModeConfig.getZenPolicySenders(policy.allowMessagesFrom()) + ? notificationPolicySendersToZenPolicyPeopleType( + policy.allowMessagesFrom()) : ZenPolicy.PEOPLE_TYPE_NONE) .allowReminders(policy.allowReminders()) .allowRepeatCallers(policy.allowRepeatCallers()) @@ -65,9 +69,58 @@ class ZenAdapters { return zenPolicyBuilder.build(); } + /** Maps {@link ZenPolicy.PeopleType} enum to {@link Policy.PrioritySenders}. */ + @Policy.PrioritySenders + public static int zenPolicyPeopleTypeToNotificationPolicySenders( + @ZenPolicy.PeopleType int zpPeopleType, @Policy.PrioritySenders int defaultResult) { + switch (zpPeopleType) { + case ZenPolicy.PEOPLE_TYPE_ANYONE: + return Policy.PRIORITY_SENDERS_ANY; + case ZenPolicy.PEOPLE_TYPE_CONTACTS: + return Policy.PRIORITY_SENDERS_CONTACTS; + case ZenPolicy.PEOPLE_TYPE_STARRED: + return Policy.PRIORITY_SENDERS_STARRED; + default: + return defaultResult; + } + } + + /** Maps {@link Policy.PrioritySenders} enum to {@link ZenPolicy.PeopleType}. */ + @ZenPolicy.PeopleType + public static int notificationPolicySendersToZenPolicyPeopleType( + @Policy.PrioritySenders int npPrioritySenders) { + switch (npPrioritySenders) { + case Policy.PRIORITY_SENDERS_ANY: + return ZenPolicy.PEOPLE_TYPE_ANYONE; + case Policy.PRIORITY_SENDERS_CONTACTS: + return ZenPolicy.PEOPLE_TYPE_CONTACTS; + case Policy.PRIORITY_SENDERS_STARRED: + default: + return ZenPolicy.PEOPLE_TYPE_STARRED; + } + } + + /** Maps {@link ZenPolicy.ConversationSenders} enum to {@link Policy.ConversationSenders}. */ + @Policy.ConversationSenders + public static int zenPolicyConversationSendersToNotificationPolicy( + @ZenPolicy.ConversationSenders int zpConversationSenders, + @Policy.ConversationSenders int defaultResult) { + switch (zpConversationSenders) { + case ZenPolicy.CONVERSATION_SENDERS_ANYONE: + return Policy.CONVERSATION_SENDERS_ANYONE; + case ZenPolicy.CONVERSATION_SENDERS_IMPORTANT: + return Policy.CONVERSATION_SENDERS_IMPORTANT; + case ZenPolicy.CONVERSATION_SENDERS_NONE: + return Policy.CONVERSATION_SENDERS_NONE; + default: + return defaultResult; + } + } + + /** Maps {@link Policy.ConversationSenders} enum to {@link ZenPolicy.ConversationSenders}. */ @ZenPolicy.ConversationSenders private static int notificationPolicyConversationSendersToZenPolicy( - int npPriorityConversationSenders) { + @Policy.ConversationSenders int npPriorityConversationSenders) { switch (npPriorityConversationSenders) { case Policy.CONVERSATION_SENDERS_ANYONE: return ZenPolicy.CONVERSATION_SENDERS_ANYONE; @@ -75,8 +128,7 @@ class ZenAdapters { return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; case Policy.CONVERSATION_SENDERS_NONE: return ZenPolicy.CONVERSATION_SENDERS_NONE; - case Policy.CONVERSATION_SENDERS_UNSET: - default: + default: // including Policy.CONVERSATION_SENDERS_UNSET return ZenPolicy.CONVERSATION_SENDERS_UNSET; } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index d9ca935b35b3..1d6dd1ebd54a 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -24,6 +24,9 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCRE import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; +import static android.service.notification.ZenAdapters.notificationPolicySendersToZenPolicyPeopleType; +import static android.service.notification.ZenAdapters.zenPolicyConversationSendersToNotificationPolicy; +import static android.service.notification.ZenAdapters.zenPolicyPeopleTypeToNotificationPolicySenders; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -1269,11 +1272,11 @@ public class ZenModeConfig implements Parcelable { public ZenPolicy toZenPolicy() { ZenPolicy.Builder builder = new ZenPolicy.Builder() .allowCalls(allowCalls - ? ZenModeConfig.getZenPolicySenders(allowCallsFrom) + ? notificationPolicySendersToZenPolicyPeopleType(allowCallsFrom) : ZenPolicy.PEOPLE_TYPE_NONE) .allowRepeatCallers(allowRepeatCallers) .allowMessages(allowMessages - ? ZenModeConfig.getZenPolicySenders(allowMessagesFrom) + ? notificationPolicySendersToZenPolicyPeopleType(allowMessagesFrom) : ZenPolicy.PEOPLE_TYPE_NONE) .allowReminders(allowReminders) .allowEvents(allowEvents) @@ -1333,14 +1336,14 @@ public class ZenModeConfig implements Parcelable { if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_MESSAGES, isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_MESSAGES, defaultPolicy))) { priorityCategories |= Policy.PRIORITY_CATEGORY_MESSAGES; - messageSenders = getNotificationPolicySenders(zenPolicy.getPriorityMessageSenders(), - messageSenders); + messageSenders = zenPolicyPeopleTypeToNotificationPolicySenders( + zenPolicy.getPriorityMessageSenders(), messageSenders); } if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS, isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_CONVERSATIONS, defaultPolicy))) { priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS; - conversationSenders = getConversationSendersWithDefault( + conversationSenders = zenPolicyConversationSendersToNotificationPolicy( zenPolicy.getPriorityConversationSenders(), conversationSenders); } else { conversationSenders = CONVERSATION_SENDERS_NONE; @@ -1349,8 +1352,8 @@ public class ZenModeConfig implements Parcelable { if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS, isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_CALLS, defaultPolicy))) { priorityCategories |= Policy.PRIORITY_CATEGORY_CALLS; - callSenders = getNotificationPolicySenders(zenPolicy.getPriorityCallSenders(), - callSenders); + callSenders = zenPolicyPeopleTypeToNotificationPolicySenders( + zenPolicy.getPriorityCallSenders(), callSenders); } if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS, @@ -1449,47 +1452,6 @@ public class ZenModeConfig implements Parcelable { return (policy.suppressedVisualEffects & visualEffect) == 0; } - private static int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders, - int defaultPolicySender) { - switch (senders) { - case ZenPolicy.PEOPLE_TYPE_ANYONE: - return Policy.PRIORITY_SENDERS_ANY; - case ZenPolicy.PEOPLE_TYPE_CONTACTS: - return Policy.PRIORITY_SENDERS_CONTACTS; - case ZenPolicy.PEOPLE_TYPE_STARRED: - return Policy.PRIORITY_SENDERS_STARRED; - default: - return defaultPolicySender; - } - } - - private static int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders, - int defaultPolicySender) { - switch (senders) { - case ZenPolicy.CONVERSATION_SENDERS_ANYONE: - case ZenPolicy.CONVERSATION_SENDERS_IMPORTANT: - case ZenPolicy.CONVERSATION_SENDERS_NONE: - return senders; - default: - return defaultPolicySender; - } - } - - /** - * Maps NotificationManager.Policy senders type to ZenPolicy.PeopleType - */ - public static @ZenPolicy.PeopleType int getZenPolicySenders(int senders) { - switch (senders) { - case Policy.PRIORITY_SENDERS_ANY: - return ZenPolicy.PEOPLE_TYPE_ANYONE; - case Policy.PRIORITY_SENDERS_CONTACTS: - return ZenPolicy.PEOPLE_TYPE_CONTACTS; - case Policy.PRIORITY_SENDERS_STARRED: - default: - return ZenPolicy.PEOPLE_TYPE_STARRED; - } - } - public Policy toNotificationPolicy() { int priorityCategories = 0; int priorityCallSenders = Policy.PRIORITY_SENDERS_CONTACTS; @@ -1524,7 +1486,7 @@ public class ZenModeConfig implements Parcelable { } priorityCallSenders = sourceToPrioritySenders(allowCallsFrom, priorityCallSenders); priorityMessageSenders = sourceToPrioritySenders(allowMessagesFrom, priorityMessageSenders); - priorityConversationSenders = getConversationSendersWithDefault( + priorityConversationSenders = zenPolicyConversationSendersToNotificationPolicy( allowConversationsFrom, priorityConversationSenders); int state = areChannelsBypassingDnd ? Policy.STATE_CHANNELS_BYPASSING_DND : 0; @@ -1559,15 +1521,6 @@ public class ZenModeConfig implements Parcelable { } } - private static int prioritySendersToSource(int prioritySenders, int def) { - switch (prioritySenders) { - case Policy.PRIORITY_SENDERS_CONTACTS: return SOURCE_CONTACT; - case Policy.PRIORITY_SENDERS_STARRED: return SOURCE_STAR; - case Policy.PRIORITY_SENDERS_ANY: return SOURCE_ANYONE; - default: return def; - } - } - private static int normalizePrioritySenders(int prioritySenders, int def) { if (!(prioritySenders == Policy.PRIORITY_SENDERS_CONTACTS || prioritySenders == Policy.PRIORITY_SENDERS_STARRED diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 68e8c726a209..67a397828fa0 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -1733,6 +1733,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { private static native long nativeCopy(long destNativePtr, long sourceNativePtr, boolean keepHistory); @CriticalNative + private static native long nativeSplit(long destNativePtr, long sourceNativePtr, int idBits); + @CriticalNative private static native int nativeGetId(long nativePtr); @CriticalNative private static native int nativeGetDeviceId(long nativePtr); @@ -3767,86 +3769,23 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** - * Splits a motion event such that it includes only a subset of pointer ids. + * Splits a motion event such that it includes only a subset of pointer IDs. + * @param idBits the bitset indicating all of the pointer IDs from this motion event that should + * be in the new split event. idBits must be a non-empty subset of the pointer IDs + * contained in this event. * @hide */ + // TODO(b/327503168): Pass downTime as a parameter to split. @UnsupportedAppUsage + @NonNull public final MotionEvent split(int idBits) { - MotionEvent ev = obtain(); - synchronized (gSharedTempLock) { - final int oldPointerCount = nativeGetPointerCount(mNativePtr); - ensureSharedTempPointerCapacity(oldPointerCount); - final PointerProperties[] pp = gSharedTempPointerProperties; - final PointerCoords[] pc = gSharedTempPointerCoords; - final int[] map = gSharedTempPointerIndexMap; - - final int oldAction = nativeGetAction(mNativePtr); - final int oldActionMasked = oldAction & ACTION_MASK; - final int oldActionPointerIndex = (oldAction & ACTION_POINTER_INDEX_MASK) - >> ACTION_POINTER_INDEX_SHIFT; - int newActionPointerIndex = -1; - int newPointerCount = 0; - for (int i = 0; i < oldPointerCount; i++) { - nativeGetPointerProperties(mNativePtr, i, pp[newPointerCount]); - final int idBit = 1 << pp[newPointerCount].id; - if ((idBit & idBits) != 0) { - if (i == oldActionPointerIndex) { - newActionPointerIndex = newPointerCount; - } - map[newPointerCount] = i; - newPointerCount += 1; - } - } - - if (newPointerCount == 0) { - throw new IllegalArgumentException("idBits did not match any ids in the event"); - } - - final int newAction; - if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) { - if (newActionPointerIndex < 0) { - // An unrelated pointer changed. - newAction = ACTION_MOVE; - } else if (newPointerCount == 1) { - // The first/last pointer went down/up. - newAction = oldActionMasked == ACTION_POINTER_DOWN - ? ACTION_DOWN - : (getFlags() & FLAG_CANCELED) == 0 ? ACTION_UP : ACTION_CANCEL; - } else { - // A secondary pointer went down/up. - newAction = oldActionMasked - | (newActionPointerIndex << ACTION_POINTER_INDEX_SHIFT); - } - } else { - // Simple up/down/cancel/move or other motion action. - newAction = oldAction; - } - - final int historySize = nativeGetHistorySize(mNativePtr); - for (int h = 0; h <= historySize; h++) { - final int historyPos = h == historySize ? HISTORY_CURRENT : h; - - for (int i = 0; i < newPointerCount; i++) { - nativeGetPointerCoords(mNativePtr, map[i], historyPos, pc[i]); - } - - final long eventTimeNanos = nativeGetEventTimeNanos(mNativePtr, historyPos); - if (h == 0) { - ev.initialize(nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr), - nativeGetDisplayId(mNativePtr), - newAction, nativeGetFlags(mNativePtr), - nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr), - nativeGetButtonState(mNativePtr), nativeGetClassification(mNativePtr), - nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr), - nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr), - nativeGetDownTimeNanos(mNativePtr), eventTimeNanos, - newPointerCount, pp, pc); - } else { - nativeAddBatch(ev.mNativePtr, eventTimeNanos, pc, 0); - } - } - return ev; + if (idBits == 0) { + throw new IllegalArgumentException( + "idBits must contain at least one pointer from this motion event"); } + MotionEvent event = obtain(); + event.mNativePtr = nativeSplit(event.mNativePtr, this.mNativePtr, idBits); + return event; } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index a7cb1696e668..02f8e6e9b810 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -3447,7 +3447,10 @@ public final class ViewRootImpl implements ViewParent, // other windows to resize/move based on the raw frame of the window, waiting until we // can finish laying out this window and get back to the window manager with the // ultimately computed insets. - insetsPending = computesInternalInsets; + insetsPending = computesInternalInsets + // If this window provides insets via params, its insets source frame can be + // updated directly without waiting for WindowSession#setInsets. + && mWindowAttributes.providedInsets == null; if (mSurfaceHolder != null) { mSurfaceHolder.mSurfaceLock.lock(); diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 64e5a5bb87a2..cf6b9e5b3bae 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -1160,12 +1160,10 @@ public final class AutofillManager { // denylist only applies to not important views if (!view.isImportantForAutofill() && isActivityDeniedForAutofill()) { - Log.d(TAG, "view is not autofillable - activity denied for autofill"); return false; } if (isActivityAllowedForAutofill()) { - Log.d(TAG, "view is autofillable - activity allowed for autofill"); return true; } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 3be76cc8a60f..985f542c9982 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1153,7 +1153,6 @@ public final class InputMethodManager { } final boolean startInput; synchronized (mH) { - mImeDispatcher.clear(); if (getBindSequenceLocked() != sequence) { return; } @@ -2909,8 +2908,6 @@ public final class InputMethodManager { * @param flags {@link #HANDWRITING_DELEGATE_FLAG_HOME_DELEGATOR_ALLOWED} or {@code 0} * @param executor The executor to run the callback on. * @param callback {@code true>} would be received if delegation was accepted. - * @return {@code true} if view belongs to allowed delegate package declared in {@link - * #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) */ diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 3fc0a305c8e6..8501474b70a6 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -175,8 +175,16 @@ public final class WebViewDelegate { /** * Adds the WebView asset path to {@link android.content.res.AssetManager}. + * If {@link android.content.res.Flags#FLAG_REGISTER_RESOURCE_PATHS} is enabled, this function + * will be a no-op because the asset paths appending work will only be handled by + * {@link android.content.res.Resources#registerResourcePaths(String, ApplicationInfo)}, + * otherwise it behaves the old way. */ public void addWebViewAssetPath(Context context) { + if (android.content.res.Flags.registerResourcePaths()) { + return; + } + final String[] newAssetPaths = WebViewFactory.getLoadedPackageInfo().applicationInfo.getAllApkPaths(); final ApplicationInfo appInfo = context.getApplicationInfo(); diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index c748a57dce74..8f1b72e90da1 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -30,6 +30,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.res.Resources; import android.os.Build; import android.os.RemoteException; import android.os.ServiceManager; @@ -93,6 +94,9 @@ public final class WebViewFactory { // error for namespace lookup public static final int LIBLOAD_FAILED_TO_FIND_NAMESPACE = 10; + // generic error for future use + static final int LIBLOAD_FAILED_OTHER = 11; + /** * Stores the timestamps at which various WebView startup events occurred in this process. */ @@ -544,8 +548,14 @@ public final class WebViewFactory { Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()"); try { sTimestamps.mAddAssetsStart = SystemClock.uptimeMillis(); - for (String newAssetPath : webViewContext.getApplicationInfo().getAllApkPaths()) { - initialApplication.getAssets().addAssetPathAsSharedLibrary(newAssetPath); + if (android.content.res.Flags.registerResourcePaths()) { + Resources.registerResourcePaths(webViewContext.getPackageName(), + webViewContext.getApplicationInfo()); + } else { + for (String newAssetPath : webViewContext.getApplicationInfo() + .getAllApkPaths()) { + initialApplication.getAssets().addAssetPathAsSharedLibrary(newAssetPath); + } } sTimestamps.mAddAssetsEnd = sTimestamps.mGetClassLoaderStart = SystemClock.uptimeMillis(); diff --git a/core/java/android/webkit/WebViewProviderResponse.java b/core/java/android/webkit/WebViewProviderResponse.java index 84e34a34f7f7..926aaff782f3 100644 --- a/core/java/android/webkit/WebViewProviderResponse.java +++ b/core/java/android/webkit/WebViewProviderResponse.java @@ -40,6 +40,7 @@ public final class WebViewProviderResponse implements Parcelable { STATUS_SUCCESS, STATUS_FAILED_WAITING_FOR_RELRO, STATUS_FAILED_LISTING_WEBVIEW_PACKAGES, + STATUS_FAILED_OTHER, }) @Retention(RetentionPolicy.SOURCE) private @interface WebViewProviderStatus {} @@ -49,6 +50,7 @@ public final class WebViewProviderResponse implements Parcelable { WebViewFactory.LIBLOAD_FAILED_WAITING_FOR_RELRO; public static final int STATUS_FAILED_LISTING_WEBVIEW_PACKAGES = WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES; + public static final int STATUS_FAILED_OTHER = WebViewFactory.LIBLOAD_FAILED_OTHER; public WebViewProviderResponse( @Nullable PackageInfo packageInfo, @WebViewProviderStatus int status) { diff --git a/core/java/android/webkit/WebViewUpdateManager.java b/core/java/android/webkit/WebViewUpdateManager.java index 8ada598d8a76..07576a2e89e4 100644 --- a/core/java/android/webkit/WebViewUpdateManager.java +++ b/core/java/android/webkit/WebViewUpdateManager.java @@ -127,7 +127,7 @@ public final class WebViewUpdateManager { * * This choice will be stored persistently. * - * @param newProvider the package name to use, or null to reset to default. + * @param newProvider the package name to use. * @return the package name which is now in use, which may not be the * requested one if it was not usable. */ @@ -155,7 +155,7 @@ public final class WebViewUpdateManager { /** * Get the WebView provider which will be used if no explicit choice has been made. * - * The default provider is not guaranteed to be currently valid/usable. + * The default provider is not guaranteed to be a valid/usable WebView implementation. * * @return the default WebView provider. */ diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 55b2251ac196..0b99df323b09 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -16,6 +16,8 @@ package android.widget; +import static android.view.flags.Flags.viewVelocityApi; + import android.annotation.ColorInt; import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; @@ -1488,6 +1490,11 @@ public class HorizontalScrollView extends FrameLayout { if (!awakenScrollBars()) { postInvalidateOnAnimation(); } + + // For variable refresh rate project to track the current velocity of this View + if (viewVelocityApi()) { + setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); + } } } @@ -1810,6 +1817,11 @@ public class HorizontalScrollView extends FrameLayout { mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, maxScroll, 0, 0, width / 2, 0); + // For variable refresh rate project to track the current velocity of this View + if (viewVelocityApi()) { + setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); + } + final boolean movingRight = velocityX > 0; View currentFocused = findFocus(); diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index a1ebde76e98e..42c2d80ea322 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -16,6 +16,8 @@ package android.widget; +import static android.view.flags.Flags.viewVelocityApi; + import android.annotation.ColorInt; import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; @@ -726,6 +728,12 @@ public class ScrollView extends FrameLayout { * isFinished() is correct. */ mScroller.computeScrollOffset(); + + // For variable refresh rate project to track the current velocity of this View + if (viewVelocityApi()) { + setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); + } + mIsBeingDragged = !mScroller.isFinished() || !mEdgeGlowBottom.isFinished() || !mEdgeGlowTop.isFinished(); // Catch the edge effect if it is active. @@ -1573,6 +1581,11 @@ public class ScrollView extends FrameLayout { // Keep on drawing until the animation has finished. postInvalidateOnAnimation(); } + + // For variable refresh rate project to track the current velocity of this View + if (viewVelocityApi()) { + setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); + } } else { if (mFlingStrictSpan != null) { mFlingStrictSpan.finish(); @@ -1884,6 +1897,10 @@ public class ScrollView extends FrameLayout { mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, Math.max(0, bottom - height), 0, height/2); + // For variable refresh rate project to track the current velocity of this View + if (viewVelocityApi()) { + setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); + } if (mFlingStrictSpan == null) { mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling"); } diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 65b59790e327..c7695187e52d 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -172,6 +172,20 @@ public class IntentForwarderActivity extends Activity { newIntent.prepareToLeaveUser(callingUserId); final CompletableFuture<ResolveInfo> targetResolveInfoFuture = mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId); + + if (isPrivateProfile(callingUserId)) { + buildAndExecuteForPrivateProfile(intentReceived, className, newIntent, callingUserId, + targetUserId); + } else { + buildAndExecute(targetResolveInfoFuture, intentReceived, className, newIntent, + callingUserId, + targetUserId, userMessage, managedProfile); + } + } + + private void buildAndExecute(CompletableFuture<ResolveInfo> targetResolveInfoFuture, + Intent intentReceived, String className, Intent newIntent, int callingUserId, + int targetUserId, String userMessage, UserInfo managedProfile) { targetResolveInfoFuture .thenApplyAsync(targetResolveInfo -> { if (isResolverActivityResolveInfo(targetResolveInfo)) { @@ -195,6 +209,23 @@ public class IntentForwarderActivity extends Activity { }, getApplicationContext().getMainExecutor()); } + private void buildAndExecuteForPrivateProfile( + Intent intentReceived, String className, Intent newIntent, int callingUserId, + int targetUserId) { + final CompletableFuture<ResolveInfo> targetResolveInfoFuture = + mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId); + targetResolveInfoFuture + .thenAcceptAsync(targetResolveInfo -> { + if (isResolverActivityResolveInfo(targetResolveInfo)) { + launchResolverActivityWithCorrectTab(intentReceived, className, newIntent, + callingUserId, targetUserId); + } else { + maybeShowUserConsentMiniResolverPrivate(targetResolveInfo, newIntent, + targetUserId); + } + }, getApplicationContext().getMainExecutor()); + } + private void maybeShowUserConsentMiniResolver( ResolveInfo target, Intent launchIntent, UserInfo managedProfile) { if (target == null || isIntentForwarderResolveInfo(target) || !isDeviceProvisioned()) { @@ -233,24 +264,70 @@ public class IntentForwarderActivity extends Activity { "Showing user consent for redirection into the managed profile for intent [%s] and " + " calling package [%s]", launchIntent, callingPackage)); - int layoutId = R.layout.miniresolver; - setContentView(layoutId); + PackageManager packageManagerForTargetUser = + createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0) + .getPackageManager(); + buildMiniResolver(target, launchIntent, targetUserId, + getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser)), + packageManagerForTargetUser); - findViewById(R.id.title_container).setElevation(0); + View telephonyInfo = findViewById(R.id.miniresolver_info_section); + + // Additional information section is work telephony specific. Therefore, it is only shown + // for telephony related intents, when all sim subscriptions are in the work profile. + if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent)) + && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType() + == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { + telephonyInfo.setVisibility(View.VISIBLE); + ((TextView) findViewById(R.id.miniresolver_info_section_text)) + .setText(getWorkTelephonyInfoSectionMessage(launchIntent)); + } else { + telephonyInfo.setVisibility(View.GONE); + } + } + + private void maybeShowUserConsentMiniResolverPrivate( + ResolveInfo target, Intent launchIntent, int targetUserId) { + if (target == null || isIntentForwarderResolveInfo(target)) { + finish(); + return; + } + + String callingPackage = getCallingPackage(); + + Log.i("IntentForwarderActivity", String.format( + "Showing user consent for redirection into the main profile for intent [%s] and " + + " calling package [%s]", + launchIntent, callingPackage)); PackageManager packageManagerForTargetUser = createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0) .getPackageManager(); + buildMiniResolver(target, launchIntent, targetUserId, + getString(R.string.miniresolver_open_in_personal, + target.loadLabel(packageManagerForTargetUser)), + packageManagerForTargetUser); + + View telephonyInfo = findViewById(R.id.miniresolver_info_section); + telephonyInfo.setVisibility(View.GONE); + } + + private void buildMiniResolver(ResolveInfo target, Intent launchIntent, int targetUserId, + String resolverTitle, PackageManager pmForTargetUser) { + int layoutId = R.layout.miniresolver; + setContentView(layoutId); + + findViewById(R.id.title_container).setElevation(0); ImageView icon = findViewById(R.id.icon); icon.setImageDrawable( - getAppIcon(target, launchIntent, targetUserId, packageManagerForTargetUser)); + getAppIcon(target, launchIntent, targetUserId, pmForTargetUser)); View buttonContainer = findViewById(R.id.button_bar_container); buttonContainer.setPadding(0, 0, 0, buttonContainer.getPaddingBottom()); ((TextView) findViewById(R.id.open_cross_profile)).setText( - getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser))); + resolverTitle); // The mini-resolver's negative button is reused in this flow to cancel the intent ((Button) findViewById(R.id.use_same_profile_browser)).setText(R.string.cancel); @@ -269,21 +346,6 @@ public class IntentForwarderActivity extends Activity { targetUserId); finish(); }); - - - View telephonyInfo = findViewById(R.id.miniresolver_info_section); - - // Additional information section is work telephony specific. Therefore, it is only shown - // for telephony related intents, when all sim subscriptions are in the work profile. - if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent)) - && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType() - == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { - telephonyInfo.setVisibility(View.VISIBLE); - ((TextView) findViewById(R.id.miniresolver_info_section_text)) - .setText(getWorkTelephonyInfoSectionMessage(launchIntent)); - } else { - telephonyInfo.setVisibility(View.GONE); - } } private Drawable getAppIcon( @@ -548,6 +610,18 @@ public class IntentForwarderActivity extends Activity { } /** + * Returns the private profile for this device or null if there is no private profile. + */ + @Nullable + private UserInfo getPrivateProfile() { + List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId()); + for (UserInfo userInfo : relatedUsers) { + if (userInfo.isPrivateProfile()) return userInfo; + } + return null; + } + + /** * Returns the userId of the profile parent or UserHandle.USER_NULL if there is * no parent. */ @@ -577,6 +651,17 @@ public class IntentForwarderActivity extends Activity { return mMetricsLogger; } + private boolean isPrivateProfile(int userId) { + UserInfo privateProfile = getPrivateProfile(); + return privateSpaceFlagsEnabled() && privateProfile != null + && privateProfile.id == userId; + } + + private boolean privateSpaceFlagsEnabled() { + return android.os.Flags.allowPrivateProfile() + && android.multiuser.Flags.enablePrivateSpaceIntentRedirection(); + } + @VisibleForTesting protected Injector createInjector() { return new InjectorImpl(); diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java index b7cb3926d936..7c9fda5c6631 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java @@ -92,7 +92,7 @@ public class WireBuffer { mIndex = 0; mStartingIndex = 0; mSize = 0; - if (expectedSize > mMaxSize) { + if (expectedSize >= mMaxSize) { resize(expectedSize); } } diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp index d5f17da0a072..83b6afa8f8f6 100644 --- a/core/jni/LayoutlibLoader.cpp +++ b/core/jni/LayoutlibLoader.cpp @@ -196,14 +196,6 @@ static vector<string> parseCsv(const string& csvString) { return result; } -static vector<string> parseCsv(JNIEnv* env, jstring csvJString) { - const char* charArray = env->GetStringUTFChars(csvJString, 0); - string csvString(charArray); - vector<string> result = parseCsv(csvString); - env->ReleaseStringUTFChars(csvJString, charArray); - return result; -} - void LayoutlibLogger(base::LogId, base::LogSeverity severity, const char* tag, const char* file, unsigned int line, const char* message) { JNIEnv* env = AndroidRuntime::getJNIEnv(); @@ -395,20 +387,28 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { jmethodID getPropertyMethod = GetStaticMethodIDOrDie(env, system, "getProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); + // Java system properties that contain LayoutLib config. The initial values in the map + // are the default values if the property is not specified. + std::unordered_map<std::string, std::string> systemProperties = + {{"core_native_classes", ""}, + {"register_properties_during_load", ""}, + {"icu.data.path", ""}, + {"use_bridge_for_logging", ""}, + {"keyboard_paths", ""}}; + + for (auto& [name, defaultValue] : systemProperties) { + jstring propertyString = + (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, + env->NewStringUTF(name.c_str()), + env->NewStringUTF(defaultValue.c_str())); + const char* propertyChars = env->GetStringUTFChars(propertyString, 0); + systemProperties[name] = string(propertyChars); + env->ReleaseStringUTFChars(propertyString, propertyChars); + } // Get the names of classes that need to register their native methods - auto nativesClassesJString = - (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF("core_native_classes"), - env->NewStringUTF("")); - vector<string> classesToRegister = parseCsv(env, nativesClassesJString); - - jstring registerProperty = - (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF( - "register_properties_during_load"), - env->NewStringUTF("")); - const char* registerPropertyString = env->GetStringUTFChars(registerProperty, 0); - if (strcmp(registerPropertyString, "true") == 0) { + vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]); + + if (systemProperties["register_properties_during_load"] == "true") { // Set the system properties first as they could be used in the static initialization of // other classes if (register_android_os_SystemProperties(env) < 0) { @@ -423,35 +423,20 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { env->CallStaticVoidMethod(bridge, setSystemPropertiesMethod); property_initialize_ro_cpu_abilist(); } - env->ReleaseStringUTFChars(registerProperty, registerPropertyString); if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) { return JNI_ERR; } - // Set the location of ICU data - auto stringPath = (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF("icu.data.path"), - env->NewStringUTF("")); - const char* path = env->GetStringUTFChars(stringPath, 0); - - if (strcmp(path, "**n/a**") != 0) { - bool icuInitialized = init_icu(path); + if (!systemProperties["icu.data.path"].empty()) { + // Set the location of ICU data + bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str()); if (!icuInitialized) { - fprintf(stderr, "Failed to initialize ICU\n"); return JNI_ERR; } - } else { - fprintf(stderr, "Skip initializing ICU\n"); } - env->ReleaseStringUTFChars(stringPath, path); - - jstring useJniProperty = - (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF("use_bridge_for_logging"), - env->NewStringUTF("")); - const char* useJniString = env->GetStringUTFChars(useJniProperty, 0); - if (strcmp(useJniString, "true") == 0) { + + if (systemProperties["use_bridge_for_logging"] == "true") { layoutLog = FindClassOrDie(env, "com/android/ide/common/rendering/api/ILayoutLog"); layoutLog = MakeGlobalRefOrDie(env, layoutLog); logMethodId = GetMethodIDOrDie(env, layoutLog, "logAndroidFramework", @@ -468,23 +453,16 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { // initialize logging, so ANDROD_LOG_TAGS env variable is respected android::base::InitLogging(nullptr, android::base::StderrLogger); } - env->ReleaseStringUTFChars(useJniProperty, useJniString); // Use English locale for number format to ensure correct parsing of floats when using strtof setlocale(LC_NUMERIC, "en_US.UTF-8"); - auto keyboardPathsJString = - (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF("keyboard_paths"), - env->NewStringUTF("")); - const char* keyboardPathsString = env->GetStringUTFChars(keyboardPathsJString, 0); - if (strcmp(keyboardPathsString, "**n/a**") != 0) { - vector<string> keyboardPaths = parseCsv(env, keyboardPathsJString); + if (!systemProperties["keyboard_paths"].empty()) { + vector<string> keyboardPaths = parseCsv(systemProperties["keyboard_paths"]); init_keyboard(env, keyboardPaths); } else { fprintf(stderr, "Skip initializing keyboard\n"); } - env->ReleaseStringUTFChars(keyboardPathsJString, keyboardPathsString); return JNI_VERSION_1_6; } diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index b39d5cf3842b..23adb8f7e2a3 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -622,6 +622,18 @@ static jlong android_view_MotionEvent_nativeCopy(jlong destNativePtr, jlong sour return reinterpret_cast<jlong>(destEvent); } +static jlong android_view_MotionEvent_nativeSplit(jlong destNativePtr, jlong sourceNativePtr, + jint idBits) { + MotionEvent* destEvent = reinterpret_cast<MotionEvent*>(destNativePtr); + if (!destEvent) { + destEvent = new MotionEvent(); + } + MotionEvent* sourceEvent = reinterpret_cast<MotionEvent*>(sourceNativePtr); + destEvent->splitFrom(*sourceEvent, static_cast<std::bitset<MAX_POINTER_ID + 1>>(idBits), + InputEvent::nextId()); + return reinterpret_cast<jlong>(destEvent); +} + static jint android_view_MotionEvent_nativeGetId(jlong nativePtr) { MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr); return event->getId(); @@ -837,6 +849,7 @@ static const JNINativeMethod gMotionEventMethods[] = { // --------------- @CriticalNative ------------------ {"nativeCopy", "(JJZ)J", (void*)android_view_MotionEvent_nativeCopy}, + {"nativeSplit", "(JJI)J", (void*)android_view_MotionEvent_nativeSplit}, {"nativeGetId", "(J)I", (void*)android_view_MotionEvent_nativeGetId}, {"nativeGetDeviceId", "(J)I", (void*)android_view_MotionEvent_nativeGetDeviceId}, {"nativeGetSource", "(J)I", (void*)android_view_MotionEvent_nativeGetSource}, diff --git a/core/proto/android/hardware/sensorprivacy.proto b/core/proto/android/hardware/sensorprivacy.proto index e368c6a98a75..53aa710d57b4 100644 --- a/core/proto/android/hardware/sensorprivacy.proto +++ b/core/proto/android/hardware/sensorprivacy.proto @@ -91,9 +91,7 @@ message SensorPrivacyIndividualEnabledSensorProto { enum StateType { ENABLED = 1; DISABLED = 2; - AUTO_DRIVER_ASSISTANCE_HELPFUL_APPS = 3; - AUTO_DRIVER_ASSISTANCE_REQUIRED_APPS = 4; - AUTO_DRIVER_ASSISTANCE_APPS = 5; + ENABLED_EXCEPT_ALLOWLISTED_APPS = 3; } // DEPRECATED diff --git a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto index 5a18d9e627e8..b75d545b1305 100644 --- a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto +++ b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto @@ -39,7 +39,7 @@ message InputMethodManagerServiceProto { optional string cur_token = 14; optional int32 cur_token_display_id = 15; optional bool system_ready = 16; - optional int32 last_switch_user_id = 17; + reserved 17; // deprecated last_switch_user_id optional bool have_connection = 18; optional bool bound_to_method = 19; optional bool is_interactive = 20; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ba9751fe1d12..52bad21e156d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8679,7 +8679,7 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> - <service android:name="com.android.server.companion.InactiveAssociationsRemovalService" + <service android:name="com.android.server.companion.association.InactiveAssociationsRemovalService" android:permission="android.permission.BIND_JOB_SERVICE"> </service> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index c882938b63ce..d89f23614179 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -301,7 +301,9 @@ granted to the system companion device manager service --> <flag name="companion" value="0x800000" /> <!-- Additional flag from base permission type: this permission will be granted to the - retail demo app, as defined by the OEM. --> + retail demo app, as defined by the OEM. + This flag has been replaced by the retail demo role and is a no-op since Android V. + --> <flag name="retailDemo" value="0x1000000" /> <!-- Additional flag from base permission type: this permission will be granted to the recents app. --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 90f27311d7c3..4f20fceef8d1 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3551,10 +3551,6 @@ <string name="config_keyguardComponent" translatable="false" >com.android.systemui/com.android.systemui.keyguard.KeyguardService</string> - <!-- Screen record dialog component --> - <string name="config_screenRecorderComponent" translatable="false" - >com.android.systemui/com.android.systemui.screenrecord.ScreenRecordDialog</string> - <!-- The component name of a special dock app that merely launches a dream. We don't want to launch this app when docked because it causes an unnecessary activity transition. We just want to start the dream. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a025c8db547f..150951fd57f8 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -375,7 +375,6 @@ <java-symbol type="string" name="config_recentsComponentName" /> <java-symbol type="string" name="config_systemUIServiceComponent" /> <java-symbol type="string" name="config_controlsPackage" /> - <java-symbol type="string" name="config_screenRecorderComponent" /> <java-symbol type="string" name="config_somnambulatorComponent" /> <java-symbol type="string" name="config_screenshotAppClipsServiceComponent" /> <java-symbol type="string" name="config_screenshotServiceComponent" /> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 61e6a36839ff..7d740ef76daf 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -88,6 +88,9 @@ <!-- Colombia: 1-6 digits (not confirmed) --> <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289" /> + <!-- Costa Rica --> + <shortcode country="cr" pattern="\\d{1,6}" free="466453" /> + <!-- Cyprus: 4-6 digits (not confirmed), known premium codes listed, plus EU --> <shortcode country="cy" pattern="\\d{4,6}" premium="7510" free="116\\d{3}" /> @@ -143,6 +146,9 @@ <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece --> <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" /> + <!-- Guatemala --> + <shortcode country="gt" pattern="\\d{1,6}" free="466453" /> + <!-- Croatia --> <shortcode country="hr" pattern="\\d{1,5}" free="13062" /> @@ -241,10 +247,10 @@ <shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" /> <!-- Pakistan --> - <shortcode country="pk" pattern="\\d{1,5}" free="2057" /> + <shortcode country="pk" pattern="\\d{1,5}" free="2057|9092" /> <!-- Palestine: 5 digits, known premium codes listed --> - <shortcode country="ps" pattern="\\d{1,5}" free="37477" /> + <shortcode country="ps" pattern="\\d{1,5}" free="37477|6681" /> <!-- Poland: 4-5 digits (not confirmed), known premium codes listed, plus EU --> <shortcode country="pl" pattern="\\d{4,5}" premium="74240|79(?:10|866)|92525" free="116\\d{3}|8012|80921" /> @@ -324,4 +330,7 @@ <!-- South Africa --> <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056" /> + <!-- Zimbabwe --> + <shortcode country="zw" pattern="\\d{1,5}" free="33679" /> + </shortcodes> diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java index 37a499adf682..6cc54850dce6 100644 --- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java +++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java @@ -110,8 +110,6 @@ public class BugreportManagerTest { Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"), Paths.get("/data/misc/wmtrace/wm_trace.winscope"), Paths.get("/data/misc/wmtrace/wm_log.winscope"), - Paths.get("/data/misc/wmtrace/wm_transition_trace.winscope"), - Paths.get("/data/misc/wmtrace/shell_transition_trace.winscope"), }; private Handler mHandler; @@ -257,6 +255,38 @@ public class BugreportManagerTest { assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles); } + @LargeTest + @Test + public void noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag() throws Exception { + startPreDumpedUiTraces(); + + mBrm.preDumpUiData(); + waitTillDumpstateExitedOrTimeout(); + + // Simulate lost of pre-dumped data. + // For example it can happen in this scenario: + // 1. Pre-dump data + // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK) + // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK) + removeFilesIfNeeded(UI_TRACES_PREDUMPED); + + // Start bugreport with "use predump" flag. Because the pre-dumped data is not available + // the flag will be ignored and data will be dumped as in normal flow. + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor, + callback); + shareConsentDialog(ConsentReply.ALLOW); + waitTillDoneOrTimeout(callback); + + stopPreDumpedUiTraces(); + + assertThat(callback.isDone()).isTrue(); + assertThat(mBugreportFile.length()).isGreaterThan(0L); + assertFdsAreClosed(mBugreportFd); + + assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED); + } + @Test public void simultaneousBugreportsNotAllowed() throws Exception { // Start bugreport #1 @@ -506,9 +536,6 @@ public class BugreportManagerTest { InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( "cmd window tracing start" ); - InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( - "service call SurfaceFlinger 1025 i32 1" - ); } private static void stopPreDumpedUiTraces() { @@ -611,6 +638,14 @@ public class BugreportManagerTest { return files; } + private static void removeFilesIfNeeded(Path[] paths) throws Exception { + for (Path path : paths) { + InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( + "rm -f " + path.toString() + ); + } + } + private static ParcelFileDescriptor parcelFd(File file) throws Exception { return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index e72beee71e50..24031cad0a3e 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -101,6 +101,7 @@ android_test { "flickerlib-trace_processor_shell", "mockito-target-extended-minus-junit4", "TestParameterInjector", + "android.content.res.flags-aconfig-java", ], libs: [ diff --git a/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml index 866e1a95c3f5..502921263462 100644 --- a/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml +++ b/core/tests/coretests/res/layout/activity_horizontal_scroll_view.xml @@ -14,105 +14,150 @@ ~ limitations under the License --> -<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:id="@+id/horizontal_scroll_view"> + android:orientation="vertical"> - <LinearLayout + <HorizontalScrollView android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> - - <View - android:background="#F00" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#880" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#0F0" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#088" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#00F" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#808" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#F00" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#880" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#0F0" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#088" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#00F" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#808" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#F00" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#880" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#0F0" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#088" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#00F" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#808" - android:layout_width="100dp" - android:layout_height="100dp" /> - - </LinearLayout> -</HorizontalScrollView> + android:layout_height="match_parent" + android:id="@+id/horizontal_scroll_view"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + </LinearLayout> + </HorizontalScrollView> + + <view + class="android.widget.HorizontalScrollViewFunctionalTest$MyHorizontalScrollView" + android:id="@+id/my_horizontal_scroll_view" + android:layout_width="90dp" + android:layout_height="90dp" + android:background="#FFF" + android:defaultFocusHighlightEnabled="false"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <View + android:background="#00F" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#0FF" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#0F0" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#FF0" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#F00" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#F0F" + android:layout_width="90dp" + android:layout_height="50dp"/> + </LinearLayout> + </view> +</LinearLayout>
\ No newline at end of file diff --git a/core/tests/coretests/res/layout/activity_scroll_view.xml b/core/tests/coretests/res/layout/activity_scroll_view.xml index 61fabf8ee437..db8cd026e71a 100644 --- a/core/tests/coretests/res/layout/activity_scroll_view.xml +++ b/core/tests/coretests/res/layout/activity_scroll_view.xml @@ -14,105 +14,150 @@ ~ limitations under the License --> -<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:id="@+id/scroll_view"> + android:orientation="vertical"> - <LinearLayout + <ScrollView android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <View - android:background="#F00" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#880" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#0F0" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#088" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#00F" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#808" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#F00" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#880" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#0F0" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#088" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#00F" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#808" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#F00" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#880" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#0F0" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#088" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#00F" - android:layout_width="100dp" - android:layout_height="100dp" /> - - <View - android:background="#808" - android:layout_width="100dp" - android:layout_height="100dp" /> - - </LinearLayout> -</ScrollView> + android:layout_height="match_parent" + android:id="@+id/scroll_view"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#F00" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#880" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#0F0" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#088" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#00F" + android:layout_width="100dp" + android:layout_height="100dp" /> + + <View + android:background="#808" + android:layout_width="100dp" + android:layout_height="100dp" /> + + </LinearLayout> + </ScrollView> + + <view + class="android.widget.ScrollViewFunctionalTest$MyScrollView" + android:id="@+id/my_scroll_view" + android:layout_width="90dp" + android:layout_height="90dp" + android:background="#FFF" + android:defaultFocusHighlightEnabled="false"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <View + android:background="#00F" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#0FF" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#0F0" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#FF0" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#F00" + android:layout_width="90dp" + android:layout_height="50dp"/> + <View + android:background="#F0F" + android:layout_width="90dp" + android:layout_height="50dp"/> + </LinearLayout> + </view> +</LinearLayout> diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java index 4a9cb7180a3f..0c1e8793bfc9 100644 --- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java +++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java @@ -18,34 +18,52 @@ package android.content.res; import android.annotation.NonNull; import android.app.ResourcesManager; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.LocaleList; import android.platform.test.annotations.Postsubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Display; import android.view.DisplayAdjustments; +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @Postsubmit +@RunWith(AndroidJUnit4.class) public class ResourcesManagerTest extends TestCase { private static final int SECONDARY_DISPLAY_ID = 1; private static final String APP_ONE_RES_DIR = "app_one.apk"; private static final String APP_ONE_RES_SPLIT_DIR = "app_one_split.apk"; private static final String APP_TWO_RES_DIR = "app_two.apk"; private static final String LIB_RES_DIR = "lib.apk"; + private static final String TEST_LIB = "com.android.frameworks.coretests.bdr_helper_app1"; private ResourcesManager mResourcesManager; private Map<Integer, DisplayMetrics> mDisplayMetricsMap; + private PackageManager mPackageManager; - @Override - protected void setUp() throws Exception { + @Before + public void setUp() throws Exception { super.setUp(); mDisplayMetricsMap = new HashMap<>(); @@ -93,8 +111,14 @@ public class ResourcesManagerTest extends TestCase { return mDisplayMetricsMap.get(displayId); } }; + + mPackageManager = InstrumentationRegistry.getContext().getPackageManager(); } + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Test @SmallTest public void testMultipleCallsWithIdenticalParametersCacheReference() { Resources resources = mResourcesManager.getResources( @@ -109,6 +133,7 @@ public class ResourcesManagerTest extends TestCase { assertSame(resources.getImpl(), newResources.getImpl()); } + @Test @SmallTest public void testMultipleCallsWithDifferentParametersReturnDifferentReferences() { Resources resources = mResourcesManager.getResources( @@ -125,6 +150,7 @@ public class ResourcesManagerTest extends TestCase { assertNotSame(resources, newResources); } + @Test @SmallTest public void testAddingASplitCreatesANewImpl() { Resources resources1 = mResourcesManager.getResources( @@ -142,6 +168,7 @@ public class ResourcesManagerTest extends TestCase { assertNotSame(resources1.getImpl(), resources2.getImpl()); } + @Test @SmallTest public void testUpdateConfigurationUpdatesAllAssetManagers() { Resources resources1 = mResourcesManager.getResources( @@ -187,6 +214,7 @@ public class ResourcesManagerTest extends TestCase { assertEquals(expectedConfig, resources3.getConfiguration()); } + @Test @SmallTest public void testTwoActivitiesWithIdenticalParametersShareImpl() { Binder activity1 = new Binder(); @@ -208,6 +236,7 @@ public class ResourcesManagerTest extends TestCase { assertSame(resources1.getImpl(), resources2.getImpl()); } + @Test @SmallTest public void testThemesGetUpdatedWithNewImpl() { Binder activity1 = new Binder(); @@ -237,6 +266,7 @@ public class ResourcesManagerTest extends TestCase { assertTrue(value.data != 0); } + @Test @SmallTest public void testMultipleResourcesForOneActivityGetUpdatedWhenActivityBaseUpdates() { Binder activity1 = new Binder(); @@ -286,6 +316,7 @@ public class ResourcesManagerTest extends TestCase { assertEquals(expectedConfig2, resources2.getConfiguration()); } + @Test @SmallTest public void testChangingActivityDisplayDoesntOverrideDisplayRequestedByResources() { Binder activity = new Binder(); @@ -322,4 +353,101 @@ public class ResourcesManagerTest extends TestCase { assertEquals(mDisplayMetricsMap.get(Display.DEFAULT_DISPLAY).widthPixels, defaultDisplayResources.getDisplayMetrics().widthPixels); } + + @Test + @SmallTest + @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS) + public void testExistingResourcesAfterResourcePathsRegistration() + throws PackageManager.NameNotFoundException { + // Inject ResourcesManager instance from this test to the ResourcesManager class so that all + // the static method can interact with this test smoothly. + ResourcesManager oriResourcesManager = ResourcesManager.getInstance(); + ResourcesManager.setInstance(mResourcesManager); + + // Create a Resources before register resources' paths for a package. + Resources resources = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, null, null, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null); + assertNotNull(resources); + ResourcesImpl oriResImpl = resources.getImpl(); + + ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0); + Resources.registerResourcePaths(TEST_LIB, appInfo); + + assertNotSame(oriResImpl, resources.getImpl()); + + String[] resourcePaths = appInfo.getAllApkPaths(); + resourcePaths = removeDuplicates(resourcePaths); + ApkAssets[] loadedAssets = resources.getAssets().getApkAssets(); + assertTrue(allResourcePathsLoaded(resourcePaths, loadedAssets)); + + // Package resources' paths should be cached in ResourcesManager. + assertEquals(Arrays.toString(resourcePaths), Arrays.toString(ResourcesManager.getInstance() + .getSharedLibAssetsMap().get(TEST_LIB).getAllAssetPaths())); + + // Revert the ResourcesManager instance back. + ResourcesManager.setInstance(oriResourcesManager); + } + + @Test + @SmallTest + @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS) + public void testNewResourcesAfterResourcePathsRegistration() + throws PackageManager.NameNotFoundException { + // Inject ResourcesManager instance from this test to the ResourcesManager class so that all + // the static method can interact with this test smoothly. + ResourcesManager oriResourcesManager = ResourcesManager.getInstance(); + ResourcesManager.setInstance(mResourcesManager); + + ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0); + Resources.registerResourcePaths(TEST_LIB, appInfo); + + // Create a Resources after register resources' paths for a package. + Resources resources = mResourcesManager.getResources( + null, APP_ONE_RES_DIR, null, null, null, null, null, null, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null); + assertNotNull(resources); + + String[] resourcePaths = appInfo.getAllApkPaths(); + resourcePaths = removeDuplicates(resourcePaths); + ApkAssets[] loadedAssets = resources.getAssets().getApkAssets(); + assertTrue(allResourcePathsLoaded(resourcePaths, loadedAssets)); + + // Package resources' paths should be cached in ResourcesManager. + assertEquals(Arrays.toString(resourcePaths), Arrays.toString(ResourcesManager.getInstance() + .getSharedLibAssetsMap().get(TEST_LIB).getAllAssetPaths())); + + // Revert the ResourcesManager instance back. + ResourcesManager.setInstance(oriResourcesManager); + } + + private static boolean allResourcePathsLoaded(String[] resourcePaths, ApkAssets[] loadedAsset) { + for (int i = 0; i < resourcePaths.length; i++) { + if (!resourcePaths[i].endsWith(".apk")) { + continue; + } + boolean found = false; + for (int j = 0; j < loadedAsset.length; j++) { + if (loadedAsset[j].getAssetPath().equals(resourcePaths[i])) { + found = true; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static String[] removeDuplicates(String[] paths) { + var pathList = new ArrayList<String>(); + var pathSet = new ArraySet<String>(); + final int pathsLen = paths.length; + for (int i = 0; i < pathsLen; i++) { + if (pathSet.add(paths[i])) { + pathList.add(paths[i]); + } + } + return pathList.toArray(new String[0]); + } } diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java index df212ebe1744..cd38bd68a26b 100644 --- a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java +++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java @@ -16,11 +16,17 @@ package android.widget; +import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.content.Context; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.AttributeSet; import android.util.PollingCheck; import androidx.test.filters.MediumTest; @@ -43,14 +49,21 @@ import java.util.concurrent.TimeUnit; public class HorizontalScrollViewFunctionalTest { private HorizontalScrollViewActivity mActivity; private HorizontalScrollView mHorizontalScrollView; + private MyHorizontalScrollView mMyHorizontalScrollView; @Rule public ActivityTestRule<HorizontalScrollViewActivity> mActivityRule = new ActivityTestRule<>( HorizontalScrollViewActivity.class); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() throws Exception { mActivity = mActivityRule.getActivity(); mHorizontalScrollView = mActivity.findViewById(R.id.horizontal_scroll_view); + mMyHorizontalScrollView = + (MyHorizontalScrollView) mActivity.findViewById(R.id.my_horizontal_scroll_view); } @Test @@ -79,6 +92,22 @@ public class HorizontalScrollViewFunctionalTest { assertEquals(maxScroll, mHorizontalScrollView.getScrollX()); } + @Test + @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API) + public void testSetVelocity() throws Throwable { + mActivityRule.runOnUiThread(() -> { + mMyHorizontalScrollView.setFrameContentVelocity(0); + }); + // set setFrameContentVelocity shouldn't do anything. + assertEquals(mMyHorizontalScrollView.isSetVelocityCalled, false); + + mActivityRule.runOnUiThread(() -> { + mMyHorizontalScrollView.fling(100); + }); + // set setFrameContentVelocity should be called when fling. + assertEquals(mMyHorizontalScrollView.isSetVelocityCalled, true); + } + static class WatchedEdgeEffect extends EdgeEffect { public CountDownLatch onAbsorbLatch = new CountDownLatch(1); @@ -92,5 +121,29 @@ public class HorizontalScrollViewFunctionalTest { onAbsorbLatch.countDown(); } } + + public static class MyHorizontalScrollView extends ScrollView { + + public boolean isSetVelocityCalled; + + public MyHorizontalScrollView(Context context) { + super(context); + } + + public MyHorizontalScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setFrameContentVelocity(float pixelsPerSecond) { + if (pixelsPerSecond != 0) { + isSetVelocityCalled = true; + } + } + } } diff --git a/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java index 109c8080de94..a60b2a13e2eb 100644 --- a/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java +++ b/core/tests/coretests/src/android/widget/ScrollViewFunctionalTest.java @@ -16,11 +16,17 @@ package android.widget; +import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.content.Context; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.AttributeSet; import android.util.PollingCheck; import androidx.test.filters.MediumTest; @@ -43,14 +49,20 @@ import java.util.concurrent.TimeUnit; public class ScrollViewFunctionalTest { private ScrollViewActivity mActivity; private ScrollView mScrollView; + private MyScrollView mMyScrollView; @Rule public ActivityTestRule<ScrollViewActivity> mActivityRule = new ActivityTestRule<>( ScrollViewActivity.class); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() throws Exception { mActivity = mActivityRule.getActivity(); mScrollView = mActivity.findViewById(R.id.scroll_view); + mMyScrollView = (MyScrollView) mActivity.findViewById(R.id.my_scroll_view); } @Test @@ -79,6 +91,22 @@ public class ScrollViewFunctionalTest { assertEquals(maxScroll, mScrollView.getScrollY()); } + @Test + @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API) + public void testSetVelocity() throws Throwable { + mActivityRule.runOnUiThread(() -> { + mMyScrollView.setFrameContentVelocity(0); + }); + // set setFrameContentVelocity shouldn't do anything. + assertEquals(mMyScrollView.isSetVelocityCalled, false); + + mActivityRule.runOnUiThread(() -> { + mMyScrollView.fling(100); + }); + // set setFrameContentVelocity should be called when fling. + assertEquals(mMyScrollView.isSetVelocityCalled, true); + } + static class WatchedEdgeEffect extends EdgeEffect { public CountDownLatch onAbsorbLatch = new CountDownLatch(1); @@ -92,5 +120,29 @@ public class ScrollViewFunctionalTest { onAbsorbLatch.countDown(); } } + + public static class MyScrollView extends ScrollView { + + public boolean isSetVelocityCalled; + + public MyScrollView(Context context) { + super(context); + } + + public MyScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public MyScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setFrameContentVelocity(float pixelsPerSecond) { + if (pixelsPerSecond != 0) { + isSetVelocityCalled = true; + } + } + } } diff --git a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java index 58cfc6625051..43e62275152e 100644 --- a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java @@ -53,6 +53,7 @@ import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import androidx.test.InstrumentationRegistry; @@ -93,6 +94,9 @@ public class IntentForwarderActivityTest { private static final String TYPE_PLAIN_TEXT = "text/plain"; private static UserInfo MANAGED_PROFILE_INFO = new UserInfo(); + private static UserInfo PRIVATE_PROFILE_INFO = new UserInfo(12, "Private", null, + UserInfo.FLAG_PROFILE, UserManager.USER_TYPE_PROFILE_PRIVATE); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); static { MANAGED_PROFILE_INFO.id = 10; @@ -131,6 +135,7 @@ public class IntentForwarderActivityTest { @Before public void setup() { + MockitoAnnotations.initMocks(this); mContext = InstrumentationRegistry.getTargetContext(); sInjector = spy(new TestInjector()); @@ -632,6 +637,54 @@ public class IntentForwarderActivityTest { logMakerCaptor.getValue().getSubtype()); } + @Test + public void shouldForwardToParent_telephony_privateProfile() throws Exception { + mSetFlagsRule.enableFlags( + android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION); + + sComponentName = FORWARD_TO_PARENT_COMPONENT_NAME; + when(mIPm.canForwardTo( + any(Intent.class), nullable(String.class), anyInt(), anyInt())).thenReturn(true); + + List<UserInfo> profiles = new ArrayList<>(); + profiles.add(CURRENT_USER_INFO); + profiles.add(PRIVATE_PROFILE_INFO); + when(mUserManager.getProfiles(anyInt())).thenReturn(profiles); + when(mUserManager.getProfileParent(anyInt())).thenReturn(CURRENT_USER_INFO); + Intent intent = new Intent(mContext, IntentForwarderWrapperActivity.class); + intent.setAction(Intent.ACTION_DIAL); + IntentForwarderWrapperActivity activity = mActivityRule.launchActivity(intent); + verify(mIPm).canForwardTo(any(), any(), anyInt(), anyInt()); + assertEquals(activity.getStartActivityIntent().getAction(), intent.getAction()); + assertEquals(activity.getUserIdActivityLaunchedIn(), CURRENT_USER_INFO.id); + } + + @Test + public void shouldForwardToParent_mms_privateProfile() throws Exception { + mSetFlagsRule.enableFlags( + android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION); + + sComponentName = FORWARD_TO_PARENT_COMPONENT_NAME; + when(mIPm.canForwardTo( + any(Intent.class), nullable(String.class), anyInt(), anyInt())).thenReturn(true); + + List<UserInfo> profiles = new ArrayList<>(); + profiles.add(CURRENT_USER_INFO); + profiles.add(PRIVATE_PROFILE_INFO); + when(mUserManager.getProfiles(anyInt())).thenReturn(profiles); + when(mUserManager.getProfileParent(anyInt())).thenReturn(CURRENT_USER_INFO); + Intent intent = new Intent(mContext, IntentForwarderWrapperActivity.class); + intent.setAction(Intent.ACTION_SEND); + intent.setType(TYPE_PLAIN_TEXT); + IntentForwarderWrapperActivity activity = mActivityRule.launchActivity(intent); + verify(mIPm).canForwardTo(any(), any(), anyInt(), anyInt()); + assertEquals(activity.getStartActivityIntent().getAction(), intent.getAction()); + assertEquals(activity.getStartActivityIntent().getType(), intent.getType()); + assertEquals(activity.getUserIdActivityLaunchedIn(), CURRENT_USER_INFO.id); + } + private void setupShouldSkipDisclosureTest() throws RemoteException { sComponentName = FORWARD_TO_PARENT_COMPONENT_NAME; sActivityName = "MyTestActivity"; @@ -688,6 +741,14 @@ public class IntentForwarderActivityTest { protected MetricsLogger getMetricsLogger() { return mMetricsLogger; } + + Intent getStartActivityIntent() { + return mStartActivityIntent; + } + + int getUserIdActivityLaunchedIn() { + return mUserIdActivityLaunchedIn; + } } public class TestInjector implements IntentForwarderActivity.Injector { diff --git a/graphics/java/android/graphics/Matrix44.java b/graphics/java/android/graphics/Matrix44.java index 7cc0eb7a6728..a99e20101c3b 100644 --- a/graphics/java/android/graphics/Matrix44.java +++ b/graphics/java/android/graphics/Matrix44.java @@ -17,6 +17,7 @@ package android.graphics; import android.annotation.FlaggedApi; +import android.annotation.IntRange; import android.annotation.NonNull; import com.android.graphics.hwui.flags.Flags; @@ -98,11 +99,11 @@ public class Matrix44 { /** * Gets the value at the matrix's row and column. * - * @param row An integer from 0 to 4 indicating the row of the value to get - * @param col An integer from 0 to 4 indicating the column of the value to get + * @param row An integer from 0 to 3 indicating the row of the value to get + * @param col An integer from 0 to 3 indicating the column of the value to get */ @FlaggedApi(Flags.FLAG_MATRIX_44) - public float get(int row, int col) { + public float get(@IntRange(from = 0, to = 3) int row, @IntRange(from = 0, to = 3) int col) { if (row >= 0 && row < 4 && col >= 0 && col < 4) { return mBackingArray[row * 4 + col]; } @@ -112,12 +113,13 @@ public class Matrix44 { /** * Sets the value at the matrix's row and column to the provided value. * - * @param row An integer from 0 to 4 indicating the row of the value to change - * @param col An integer from 0 to 4 indicating the column of the value to change + * @param row An integer from 0 to 3 indicating the row of the value to change + * @param col An integer from 0 to 3 indicating the column of the value to change * @param val The value the element at the specified index will be set to */ @FlaggedApi(Flags.FLAG_MATRIX_44) - public void set(int row, int col, float val) { + public void set(@IntRange(from = 0, to = 3) int row, @IntRange(from = 0, to = 3) int col, + float val) { if (row >= 0 && row < 4 && col >= 0 && col < 4) { mBackingArray[row * 4 + col] = val; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 8fd6ffe15cfe..474430eb44ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -717,11 +717,6 @@ public class BubbleStackView extends FrameLayout // Hide the stack after a delay, if needed. updateTemporarilyInvisibleAnimation(false /* hideImmediately */); - - if (mShouldReorderBubblesAfterGestureCompletes) { - mShouldReorderBubblesAfterGestureCompletes = false; - updateBubbleOrderInternal(mBubbleData.getBubbles(), true); - } } }; @@ -2732,6 +2727,12 @@ public class BubbleStackView extends FrameLayout ev.getAction() != MotionEvent.ACTION_UP && ev.getAction() != MotionEvent.ACTION_CANCEL; + // If there is a deferred reorder action, and the gesture is over, run it now. + if (mShouldReorderBubblesAfterGestureCompletes && !mIsGestureInProgress) { + mShouldReorderBubblesAfterGestureCompletes = false; + updateBubbleOrderInternal(mBubbleData.getBubbles(), false); + } + return dispatched; } diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp index 2ea4e3f21163..af169f4bc4cd 100644 --- a/libs/hwui/VectorDrawable.cpp +++ b/libs/hwui/VectorDrawable.cpp @@ -540,7 +540,7 @@ bool Tree::allocateBitmapIfNeeded(Cache& cache, int width, int height) { } bool Tree::canReuseBitmap(Bitmap* bitmap, int width, int height) { - return bitmap && width <= bitmap->width() && height <= bitmap->height(); + return bitmap && width == bitmap->width() && height == bitmap->height(); } void Tree::onPropertyChanged(TreeProperties* prop) { diff --git a/media/java/android/media/metrics/PlaybackSession.java b/media/java/android/media/metrics/PlaybackSession.java index f8dd756c5f9c..6223acf8ffa4 100644 --- a/media/java/android/media/metrics/PlaybackSession.java +++ b/media/java/android/media/metrics/PlaybackSession.java @@ -24,7 +24,10 @@ import com.android.internal.util.AnnotationValidations; import java.util.Objects; /** - * An instances of this class represents a session of media playback. + * An instance of this class represents a session of media playback used to report playback + * metrics and events. + * + * Create a new instance using {@link MediaMetricsManager#createPlaybackSession}. */ public final class PlaybackSession implements AutoCloseable { private final @NonNull String mId; @@ -80,6 +83,21 @@ public final class PlaybackSession implements AutoCloseable { mManager.reportTrackChangeEvent(mId, event); } + /** + * A session ID is used to identify a unique playback and to tie together lower-level + * playback components. + * + * Associate this session with a {@link MediaCodec} by passing the ID into + * {@link MediaFormat} through {@link MediaFormat#LOG_SESSION_ID} when + * creating the {@link MediaCodec}. + * + * Associate this session with an {@link AudioTrack} by calling + * {@link AudioTrack#setLogSessionId}. + * + * Associate this session with {@link MediaDrm} and {@link MediaCrypto} by calling + * {@link MediaDrm#getPlaybackComponent} and then calling + * {@link PlaybackComponent#setLogSessionId}. + */ public @NonNull LogSessionId getSessionId() { return mLogSessionId; } diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 8396005b1b63..0fc80dd55fa9 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -2895,6 +2895,10 @@ static void extractBufferFromContext( jint offset, jint size, std::shared_ptr<C2Buffer> *buffer) { + if ((offset + size) > context->capacity()) { + ALOGW("extractBufferFromContext: offset + size provided exceed capacity"); + return; + } *buffer = context->toC2Buffer(offset, size); if (*buffer == nullptr) { if (!context->mMemory) { @@ -2995,18 +2999,15 @@ static void android_media_MediaCodec_native_queueLinearBlock( "MediaCodec.LinearBlock#obtain method to obtain a compatible buffer."); return; } - sp<CryptoInfosWrapper> cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()}; - jint sampleSize = 0; + sp<CryptoInfosWrapper> cryptoInfos = nullptr; + jint sampleSize = totalSize; if (cryptoInfoArray != nullptr) { + cryptoInfos = new CryptoInfosWrapper{decltype(cryptoInfos->value)()}; extractCryptoInfosFromObjectArray(env, &sampleSize, &cryptoInfos->value, cryptoInfoArray, &errorDetailMsg); - } else { - sampleSize = totalSize; - std::unique_ptr<CodecCryptoInfo> cryptoInfo{new MediaCodecCryptoInfo(totalSize)}; - cryptoInfos->value.push_back(std::move(cryptoInfo)); } if (env->ExceptionCheck()) { // Creation of cryptoInfo failed. Let the exception bubble up. diff --git a/native/android/Android.bp b/native/android/Android.bp index 7f3792d06795..752ebdf3d0e3 100644 --- a/native/android/Android.bp +++ b/native/android/Android.bp @@ -98,6 +98,7 @@ cc_library_shared { "libpowermanager", "android.hardware.configstore@1.0", "android.hardware.configstore-utils", + "android.os.flags-aconfig-cc", "libnativedisplay", ], diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 9d0221a3ae68..c0db089253e7 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -268,9 +268,9 @@ package android.nfc.cardemulation { ctor public PollingFrame(int, @Nullable byte[], int, int); method public int describeContents(); method @NonNull public byte[] getData(); - method public int getGain(); method public int getTimestamp(); method public int getType(); + method public int getVendorSpecificGain(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.PollingFrame> CREATOR; field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final int POLLING_LOOP_TYPE_A = 65; // 0x41 diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java index 994f4ae1c2e3..29d7bdf37fe4 100644 --- a/nfc/java/android/nfc/cardemulation/PollingFrame.java +++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java @@ -157,7 +157,7 @@ public final class PollingFrame implements Parcelable{ mType = frame.getInt(KEY_POLLING_LOOP_TYPE); byte[] data = frame.getByteArray(KEY_POLLING_LOOP_DATA); mData = (data == null) ? new byte[0] : data; - mGain = frame.getByte(KEY_POLLING_LOOP_GAIN); + mGain = frame.getInt(KEY_POLLING_LOOP_GAIN, -1); mTimestamp = frame.getInt(KEY_POLLING_LOOP_TIMESTAMP); } @@ -194,8 +194,9 @@ public final class PollingFrame implements Parcelable{ /** * Returns the gain representing the field strength of the NFC field when this polling loop * frame was observed. + * @return the gain or -1 if there is no gain measurement associated with this frame. */ - public int getGain() { + public int getVendorSpecificGain() { return mGain; } @@ -227,7 +228,9 @@ public final class PollingFrame implements Parcelable{ public Bundle toBundle() { Bundle frame = new Bundle(); frame.putInt(KEY_POLLING_LOOP_TYPE, getType()); - frame.putByte(KEY_POLLING_LOOP_GAIN, (byte) getGain()); + if (getVendorSpecificGain() != -1) { + frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain()); + } frame.putByteArray(KEY_POLLING_LOOP_DATA, getData()); frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp()); return frame; @@ -236,7 +239,7 @@ public final class PollingFrame implements Parcelable{ @Override public String toString() { return "PollingFrame { Type: " + (char) getType() - + ", gain: " + getGain() + + ", gain: " + getVendorSpecificGain() + ", timestamp: " + Integer.toUnsignedString(getTimestamp()) + ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }"; } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 4e1f4ee2e565..3363ac025f12 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -29,6 +29,7 @@ import android.credentials.CredentialOption import android.credentials.selection.Entry import android.credentials.selection.GetCredentialProviderData import android.credentials.selection.ProviderData +import android.graphics.BlendMode import android.graphics.drawable.Icon import android.os.Bundle import android.os.CancellationSignal @@ -353,6 +354,7 @@ class CredentialAutofillService : AutofillService() { val sliceBuilder = InlineSuggestionUi .newContentBuilder(pendingIntent) .setTitle(displayName) + icon.setTintBlendMode(BlendMode.DST) sliceBuilder.setStartIcon(icon) if (primaryEntry.credentialType == CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 56bd06618684..965ee860672e 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -152,14 +152,14 @@ fun Entry( }, ) } - } else if (entrySecondLineText != null) { + } else if (!entrySecondLineText.isNullOrBlank()) { BodySmallText( text = entrySecondLineText, enforceOneLine = enforceOneLine, onTextLayout = onTextLayout, ) } - if (entryThirdLineText != null) { + if (!entryThirdLineText.isNullOrBlank()) { BodySmallText( text = entryThirdLineText, enforceOneLine = enforceOneLine, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt index 3ebdd204b640..ff421bc47511 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/InlinePresentationFactory.kt @@ -21,63 +21,20 @@ package com.android.credentialmanager.common.ui import android.content.Context import android.util.Size import android.widget.inline.InlinePresentationSpec -import androidx.autofill.inline.common.TextViewStyle -import androidx.autofill.inline.common.ViewStyle -import androidx.autofill.inline.UiVersions -import androidx.autofill.inline.UiVersions.Style -import androidx.autofill.inline.v1.InlineSuggestionUi -import androidx.core.content.ContextCompat -import android.util.TypedValue -import android.graphics.Typeface - class InlinePresentationsFactory { companion object { - private const val googleSansMediumFontFamily = "google-sans-medium" - private const val googleSansTextFontFamily = "google-sans-text" - // There is no min width required for now but this is needed for the spec builder - private const val minInlineWidth = 5000 + // There is no max width required for now but this is needed for the spec builder + private const val maxInlineWidth = 5000 fun modifyInlinePresentationSpec(context: Context, originalSpec: InlinePresentationSpec): InlinePresentationSpec { return InlinePresentationSpec.Builder(Size(originalSpec.minSize.width, originalSpec .minSize.height), - Size(minInlineWidth, originalSpec + Size(maxInlineWidth, originalSpec .maxSize.height)) - .setStyle(UiVersions.newStylesBuilder().addStyle(getStyle(context)).build()) - .build() - } - - - fun getStyle(context: Context): Style { - val textColorPrimary = ContextCompat.getColor(context, - com.android.credentialmanager.R.color.text_primary) - val textColorSecondary = ContextCompat.getColor(context, - com.android.credentialmanager.R.color.text_secondary) - val textColorBackground = ContextCompat.getColor(context, - com.android.credentialmanager.R.color.inline_background) - val chipHorizontalPadding = context.resources.getDimensionPixelSize(com.android - .credentialmanager.R.dimen.horizontal_chip_padding) - val chipVerticalPadding = context.resources.getDimensionPixelSize(com.android - .credentialmanager.R.dimen.vertical_chip_padding) - return InlineSuggestionUi.newStyleBuilder() - .setChipStyle( - ViewStyle.Builder().setPadding(chipHorizontalPadding, - chipVerticalPadding, - chipHorizontalPadding, chipVerticalPadding).build() - ) - .setTitleStyle( - TextViewStyle.Builder().setTextColor(textColorPrimary).setTextSize - (TypedValue.COMPLEX_UNIT_DIP, 14F) - .setTypeface(googleSansMediumFontFamily, - Typeface.NORMAL).setBackgroundColor(textColorBackground) - .build() - ) - .setSubtitleStyle(TextViewStyle.Builder().setTextColor(textColorSecondary) - .setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12F).setTypeface - (googleSansTextFontFamily, Typeface.NORMAL).setBackgroundColor - (textColorBackground).build()) + .setStyle(originalSpec.getStyle()) .build() } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 4ef776099119..af78573ee9e9 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -336,7 +336,7 @@ fun CreationSelectionCard( if (!footerDescription.isNullOrBlank()) { item { Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { - BodySmallText(text = footerDescription) + BodyMediumText(text = footerDescription) } } item { Divider(thickness = 24.dp, color = Color.Transparent) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 748c79891a40..bc0ea02d5b15 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -786,16 +786,16 @@ fun CredentialEntryRow( else if (credentialEntryInfo.icon == null) painterResource(R.drawable.ic_other_sign_in_24) else null, entryHeadlineText = username, - entrySecondLineText = + entrySecondLineText = displayName, + entryThirdLineText = (if (hasSingleEntry != null && hasSingleEntry) if (credentialEntryInfo.credentialType == CredentialType.PASSKEY || credentialEntryInfo.credentialType == CredentialType.PASSWORD) - listOf(displayName) + emptyList() // Still show the type display name for all non-password/passkey types since it won't be // mentioned in the bottom sheet heading. - else listOf(displayName, credentialEntryInfo.credentialTypeDisplayName) + else listOf(credentialEntryInfo.credentialTypeDisplayName) else listOf( - displayName, credentialEntryInfo.credentialTypeDisplayName, credentialEntryInfo.providerDisplayName )).filterNot(TextUtils::isEmpty).let { itemsToDisplay -> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index ef4018833721..e35acae547a6 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -25,6 +25,7 @@ import com.android.credentialmanager.model.get.AuthenticationEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.model.get.RemoteEntryInfo import com.android.internal.util.Preconditions +import java.time.Instant data class GetCredentialUiState( val isRequestForAllOptions: Boolean, @@ -156,11 +157,17 @@ fun toProviderDisplayInfo( userNameToCredentialEntryMap.values.forEach { it.sortWith(comparator) } - // Transform to list of PerUserNameCredentialEntryLists and then sort across usernames + // Transform to list of PerUserNameCredentialEntryLists and then sort the outer list (of + // entries grouped by username / entryGroupId) based on the latest timestamp within that + // PerUserNameCredentialEntryList val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map { PerUserNameCredentialEntryList(it.key, it.value) }.sortedWith( - compareByDescending { it.sortedCredentialEntryList.first().lastUsedTimeMillis } + compareByDescending { + it.sortedCredentialEntryList.maxByOrNull{ entry -> + entry.lastUsedTimeMillis ?: Instant.MIN + }?.lastUsedTimeMillis ?: Instant.MIN + } ) return ProviderDisplayInfo( @@ -211,7 +218,7 @@ internal class CredentialEntryInfoComparatorByTypeThenTimestamp( val typePriorityMap: Map<String, Int>, ) : Comparator<CredentialEntryInfo> { override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int { - // First prefer passkey type for its security benefits + // First rank by priorities of each credential type. if (p0.rawCredentialType != p1.rawCredentialType) { val p0Priority = typePriorityMap.getOrDefault( p0.rawCredentialType, PriorityHints.PRIORITY_DEFAULT @@ -225,6 +232,7 @@ internal class CredentialEntryInfoComparatorByTypeThenTimestamp( return 1 } } + // Then rank by last used timestamps. val p0LastUsedTimeMillis = p0.lastUsedTimeMillis val p1LastUsedTimeMillis = p1.lastUsedTimeMillis // Then order by last used timestamp diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java index 634e067a12d4..cf2f85ed5356 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java @@ -20,7 +20,6 @@ import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH; import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID; -import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -28,10 +27,10 @@ import android.app.DialogFragment; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.Flags; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.res.AssetFileDescriptor; +import android.Manifest; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -201,7 +200,7 @@ public class InstallStaging extends Activity { params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT, PackageInstaller.SessionParams.PERMISSION_STATE_DENIED); - if (pfd != null && Flags.readInstallInfo()) { + if (pfd != null) { try { final PackageInstaller.InstallInfo result = installer.readInstallInfo(pfd, debugPathName, 0); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index e95a8e63d644..45bfe5469172 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -31,7 +31,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.Flags; import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; @@ -400,10 +399,7 @@ public class PackageInstallerActivity extends Activity { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); final SessionInfo info = mInstaller.getSessionInfo(sessionId); - String resolvedPath = null; - if (info != null && Flags.getResolvedApkPath()) { - resolvedPath = info.getResolvedBaseApkPath(); - } + String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null; if (info == null || !info.isSealed() || resolvedPath == null) { Log.w(TAG, "Session " + sessionId + " in funky state; ignoring"); finish(); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 22caabdabbf0..aeabbd53d177 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -25,7 +25,6 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo -import android.content.pm.Flags import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.SessionInfo @@ -363,7 +362,7 @@ class InstallRepository(private val context: Context) { params.setPermissionState( Manifest.permission.USE_FULL_SCREEN_INTENT, SessionParams.PERMISSION_STATE_DENIED ) - if (pfd != null && Flags.readInstallInfo()) { + if (pfd != null) { try { val installInfo = packageInstaller.readInstallInfo(pfd, debugPathName, 0) params.setAppPackageName(installInfo.packageName) @@ -426,8 +425,7 @@ class InstallRepository(private val context: Context) { if (PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action) { val info = packageInstaller.getSessionInfo(sessionId) - val resolvedPath = - if (Flags.getResolvedApkPath()) info?.resolvedBaseApkPath else null + val resolvedPath = info?.resolvedBaseApkPath if (info == null || !info.isSealed || resolvedPath == null) { Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring") return InstallAborted(ABORT_REASON_INTERNAL_ERROR) diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt index 0117ece888c0..d5444cf44b02 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt @@ -53,7 +53,7 @@ open class WifiUtils { * @param noInternet True if a connected Wi-Fi network cannot access the Internet * @param level The number of bars to show (0-4) */ - fun getIcon(noInternet: Boolean, level: Int): Drawable? { + open fun getIcon(noInternet: Boolean, level: Int): Drawable? { return context.getDrawable(getInternetIconResource(level, noInternet)) } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index 827d8fa9e7c1..3b18aa310c91 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -72,6 +72,7 @@ import com.android.settingslib.testutils.shadow.ShadowUserManager; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -499,6 +500,7 @@ public class ApplicationsStateRoboTest { verify(mApplicationsState, never()).clearEntries(); } + @Ignore("b/328332487") @Test public void removeProfileApp_workprofileExists_doResumeIfNeededLocked_shouldClearEntries() throws RemoteException { @@ -573,6 +575,7 @@ public class ApplicationsStateRoboTest { verify(mApplicationsState).clearEntries(); } + @Ignore("b/328332487") @Test public void removeOwnerApp_workprofileExists_doResumeIfNeededLocked_shouldClearEntries() throws RemoteException { @@ -654,6 +657,7 @@ public class ApplicationsStateRoboTest { verify(mApplicationsState).clearEntries(); } + @Ignore("b/328332487") @Test public void noAppRemoved_workprofileExists_doResumeIfNeededLocked_shouldNotClearEntries() throws RemoteException { @@ -773,6 +777,7 @@ public class ApplicationsStateRoboTest { assertThat(primaryUserApp.shouldShowInPersonalTab(um, appInfo.uid)).isTrue(); } + @Ignore("b/328332487") @Test public void shouldShowInPersonalTab_userProfilePreU_returnsFalse() { UserManager um = RuntimeEnvironment.application.getSystemService(UserManager.class); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index add313419c7d..3043d54b3025 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -116,5 +116,8 @@ public class GlobalSettings { Settings.Global.Wearable.CHARGING_SOUNDS_ENABLED, Settings.Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, Settings.Global.FORCE_ENABLE_PSS_PROFILING, + Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED, + Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE, + Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED, }; } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index ce0257f6c85b..2a8eb9bc0845 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -72,6 +72,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -157,6 +158,9 @@ final class SettingsState { "/product/etc/aconfig_flags.pb", "/vendor/etc/aconfig_flags.pb"); + private static final String APEX_DIR = "/apex"; + private static final String APEX_ACONFIG_PATH_SUFFIX = "/etc/aconfig_flags.pb"; + /** * This tag is applied to all aconfig default value-loaded flags. */ @@ -238,7 +242,7 @@ final class SettingsState { private int mNextHistoricalOpIdx; @GuardedBy("mLock") - @Nullable + @NonNull private Map<String, Map<String, String>> mNamespaceDefaults; public static final int SETTINGS_TYPE_GLOBAL = 0; @@ -332,23 +336,29 @@ final class SettingsState { mHistoricalOperations = Build.IS_DEBUGGABLE ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null; + mNamespaceDefaults = new HashMap<>(); + synchronized (mLock) { readStateSyncLocked(); if (Flags.loadAconfigDefaults()) { if (isConfigSettingsKey(mKey)) { - loadAconfigDefaultValuesLocked(); + loadAconfigDefaultValuesLocked(sAconfigTextProtoFilesOnDevice); } } + if (Flags.loadApexAconfigProtobufs()) { + if (isConfigSettingsKey(mKey)) { + List<String> apexProtoPaths = listApexProtoPaths(); + loadAconfigDefaultValuesLocked(apexProtoPaths); + } + } } } @GuardedBy("mLock") - private void loadAconfigDefaultValuesLocked() { - mNamespaceDefaults = new HashMap<>(); - - for (String fileName : sAconfigTextProtoFilesOnDevice) { + private void loadAconfigDefaultValuesLocked(List<String> filePaths) { + for (String fileName : filePaths) { try (FileInputStream inputStream = new FileInputStream(fileName)) { loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults); } catch (IOException e) { @@ -357,13 +367,41 @@ final class SettingsState { } } + private List<String> listApexProtoPaths() { + LinkedList<String> paths = new LinkedList(); + + File apexDirectory = new File(APEX_DIR); + if (!apexDirectory.isDirectory()) { + return paths; + } + + File[] subdirs = apexDirectory.listFiles(); + if (subdirs == null) { + return paths; + } + + for (File prefix : subdirs) { + // For each mainline modules, there are two directories, one <modulepackage>/, + // and one <modulepackage>@<versioncode>/. Just read the former. + if (prefix.getAbsolutePath().contains("@")) { + continue; + } + + File protoPath = new File(prefix + APEX_ACONFIG_PATH_SUFFIX); + if (!protoPath.exists()) { + continue; + } + + paths.add(protoPath.getAbsolutePath()); + } + return paths; + } + @VisibleForTesting @GuardedBy("mLock") public void addAconfigDefaultValuesFromMap( @NonNull Map<String, Map<String, String>> defaultMap) { - if (mNamespaceDefaults != null) { - mNamespaceDefaults.putAll(defaultMap); - } + mNamespaceDefaults.putAll(defaultMap); } @VisibleForTesting @@ -447,7 +485,7 @@ final class SettingsState { return names; } - @Nullable + @NonNull public Map<String, Map<String, String>> getAconfigDefaultValues() { synchronized (mLock) { return mNamespaceDefaults; @@ -519,9 +557,9 @@ final class SettingsState { return false; } - // Aconfig flags are always boot stable, so we anytime we write one, we staged it to be + // Aconfig flags are always boot stable, so we anytime we write one, we stage it to be // applied on reboot. - if (Flags.stageAllAconfigFlags() && mNamespaceDefaults != null) { + if (Flags.stageAllAconfigFlags()) { int slashIndex = name.indexOf("/"); boolean stageFlag = isConfigSettingsKey(mKey) && slashIndex != -1 diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index e5086e87173a..2e14e9b8be4c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -25,3 +25,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "load_apex_aconfig_protobufs" + namespace: "core_experiments_team_internal" + description: "When enabled, loads aconfig default values in apex flag protobufs into DeviceConfig on boot." + bug: "327383546" + is_fixed_read_only: true +} diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 28cdc6db192b..09d076ee9c26 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -626,9 +626,6 @@ public class SettingsBackupTest { Settings.Global.Wearable.BEDTIME_MODE, Settings.Global.Wearable.BEDTIME_HARD_MODE, Settings.Global.Wearable.LOCK_SCREEN_STATE, - Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_ENABLED, - Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_TYPE, - Settings.Global.Wearable.ACCESSIBILITY_VIBRATION_WATCH_SPEED, Settings.Global.Wearable.DISABLE_AOD_WHILE_PLUGGED, Settings.Global.Wearable.NETWORK_LOCATION_OPT_IN, Settings.Global.Wearable.CUSTOM_COLOR_FOREGROUND, diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index ea30c69b1c45..33362a22ffba 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -25,7 +25,6 @@ import android.aconfig.Aconfig; import android.aconfig.Aconfig.parsed_flag; import android.aconfig.Aconfig.parsed_flags; import android.os.Looper; -import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -231,38 +230,6 @@ public class SettingsStateTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_LOAD_ACONFIG_DEFAULTS) - public void testAddingAconfigMapOnNullIsNoOp() { - int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); - Object lock = new Object(); - SettingsState settingsState = new SettingsState( - InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - - parsed_flags flags = parsed_flags - .newBuilder() - .addParsedFlag(parsed_flag - .newBuilder() - .setPackage("com.android.flags") - .setName("flag5") - .setNamespace("test_namespace") - .setDescription("test flag") - .addBug("12345678") - .setState(Aconfig.flag_state.DISABLED) - .setPermission(Aconfig.flag_permission.READ_WRITE)) - .build(); - - synchronized (lock) { - Map<String, Map<String, String>> defaults = new HashMap<>(); - settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); - settingsState.addAconfigDefaultValuesFromMap(defaults); - - assertEquals(null, settingsState.getAconfigDefaultValues()); - } - - } - - @Test public void testInvalidAconfigProtoDoesNotCrash() { Map<String, Map<String, String>> defaults = new HashMap<>(); SettingsState settingsState = getSettingStateObject(); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index cc2e84c5a995..04cb88d5a39d 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -308,6 +308,7 @@ android_library { name: "SystemUI-tests", use_resource_processor: true, manifest: "tests/AndroidManifest-base.xml", + resource_dirs: [], additional_manifests: ["tests/AndroidManifest.xml"], srcs: [ "tests/src/**/*.kt", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8c8975fdbdd5..d05d40d969de 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -25,6 +25,16 @@ flag { } flag { + name: "notification_view_flipper_pausing" + namespace: "systemui" + description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed." + bug: "309146176" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_async_group_header_inflation" namespace: "systemui" description: "Inflates the notification group summary header views from the background thread." @@ -194,14 +204,6 @@ flag { } flag { - name: "keyguard_shade_migration_nssl" - namespace: "systemui" - description: "Moves NSSL into a shared element between the notification_panel and " - "keyguard_root_view." - bug: "278054201" -} - -flag { name: "unfold_animation_background_progress" namespace: "systemui" description: "Moves unfold animation progress calculation to a background thread" @@ -419,6 +421,13 @@ flag { } flag { + name: "smartspace_remoteviews_rendering" + namespace: "systemui" + description: "Indicate Smartspace RemoteViews rendering" + bug: "326292691" +} + +flag { name: "pin_input_field_styled_focus_state" namespace: "systemui" description: "Enables styled focus states on pin input field if keyboard is connected" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index 8dc74951d332..b1240252796f 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -27,6 +27,7 @@ import android.graphics.fonts.FontVariationAxis import android.text.Layout import android.util.LruCache import kotlin.math.roundToInt +import android.util.Log private const val DEFAULT_ANIMATION_DURATION: Long = 300 private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 @@ -140,7 +141,6 @@ class TextAnimator( } sealed class PositionedGlyph { - /** Mutable X coordinate of the glyph position relative from drawing offset. */ var x: Float = 0f @@ -269,41 +269,53 @@ class TextAnimator( duration: Long = -1L, interpolator: TimeInterpolator? = null, delay: Long = 0, - onAnimationEnd: Runnable? = null + onAnimationEnd: Runnable? = null, + ) = setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration, + interpolator, delay, onAnimationEnd, updateLayoutOnFailure = true) + + private fun setTextStyleInternal( + fvar: String?, + textSize: Float, + color: Int?, + strokeWidth: Float, + animate: Boolean, + duration: Long, + interpolator: TimeInterpolator?, + delay: Long, + onAnimationEnd: Runnable?, + updateLayoutOnFailure: Boolean, ) { - if (animate) { - animator.cancel() - textInterpolator.rebase() - } - - if (textSize >= 0) { - textInterpolator.targetPaint.textSize = textSize - } - - if (!fvar.isNullOrBlank()) { - textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar) - } + try { + if (animate) { + animator.cancel() + textInterpolator.rebase() + } - if (color != null) { - textInterpolator.targetPaint.color = color - } - if (strokeWidth >= 0F) { - textInterpolator.targetPaint.strokeWidth = strokeWidth - } - textInterpolator.onTargetPaintModified() - - if (animate) { - animator.startDelay = delay - animator.duration = - if (duration == -1L) { - DEFAULT_ANIMATION_DURATION - } else { - duration - } - interpolator?.let { animator.interpolator = it } - if (onAnimationEnd != null) { - val listener = - object : AnimatorListenerAdapter() { + if (textSize >= 0) { + textInterpolator.targetPaint.textSize = textSize + } + if (!fvar.isNullOrBlank()) { + textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar) + } + if (color != null) { + textInterpolator.targetPaint.color = color + } + if (strokeWidth >= 0F) { + textInterpolator.targetPaint.strokeWidth = strokeWidth + } + textInterpolator.onTargetPaintModified() + + if (animate) { + animator.startDelay = delay + animator.duration = + if (duration == -1L) { + DEFAULT_ANIMATION_DURATION + } else { + duration + } + interpolator?.let { animator.interpolator = it } + if (onAnimationEnd != null) { + val listener = object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { onAnimationEnd.run() animator.removeListener(this) @@ -312,14 +324,25 @@ class TextAnimator( animator.removeListener(this) } } - animator.addListener(listener) + animator.addListener(listener) + } + animator.start() + } else { + // No animation is requested, thus set base and target state to the same state. + textInterpolator.progress = 1f + textInterpolator.rebase() + invalidateCallback() + } + } catch (ex: IllegalArgumentException) { + if (updateLayoutOnFailure) { + Log.e(TAG, "setTextStyleInternal: Exception caught but retrying. This is usually" + + " due to the layout having changed unexpectedly without being notified.", ex) + updateLayout(textInterpolator.layout) + setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration, + interpolator, delay, onAnimationEnd, updateLayoutOnFailure = false) + } else { + throw ex } - animator.start() - } else { - // No animation is requested, thus set base and target state to the same state. - textInterpolator.progress = 1f - textInterpolator.rebase() - invalidateCallback() } } @@ -355,15 +378,13 @@ class TextAnimator( interpolator: TimeInterpolator? = null, delay: Long = 0, onAnimationEnd: Runnable? = null - ) { - val fvar = fontVariationUtils.updateFontVariation( - weight = weight, - width = width, - opticalSize = opticalSize, - roundness = roundness, - ) - setTextStyle( - fvar = fvar, + ) = setTextStyleInternal( + fvar = fontVariationUtils.updateFontVariation( + weight = weight, + width = width, + opticalSize = opticalSize, + roundness = roundness, + ), textSize = textSize, color = color, strokeWidth = strokeWidth, @@ -372,6 +393,10 @@ class TextAnimator( interpolator = interpolator, delay = delay, onAnimationEnd = onAnimationEnd, + updateLayoutOnFailure = true, ) + + companion object { + private val TAG = TextAnimator::class.simpleName!! } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index bd539a740e81..2a13d4931b69 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.android.compose.PlatformIconButton import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel +import com.android.systemui.common.ui.compose.SelectedUserAwareInputConnection import com.android.systemui.res.R /** UI for the input part of a password-requiring version of the bouncer. */ @@ -71,6 +72,7 @@ internal fun PasswordBouncer( val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState() + val selectedUserId by viewModel.selectedUserId.collectAsState() DisposableEffect(Unit) { viewModel.onShown() @@ -87,47 +89,51 @@ internal fun PasswordBouncer( val color = MaterialTheme.colorScheme.onSurfaceVariant val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() } - TextField( - value = password, - onValueChange = viewModel::onPasswordInputChanged, - enabled = isInputEnabled, - visualTransformation = PasswordVisualTransformation(), - singleLine = true, - textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), - keyboardOptions = - KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = - KeyboardActions( - onDone = { viewModel.onAuthenticateKeyPressed() }, - ), - modifier = - modifier - .focusRequester(focusRequester) - .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) } - .drawBehind { - drawLine( - color = color, - start = Offset(x = 0f, y = size.height - lineWidthPx), - end = Offset(size.width, y = size.height - lineWidthPx), - strokeWidth = lineWidthPx, - ) - } - .onInterceptKeyBeforeSoftKeyboard { keyEvent -> - if (keyEvent.key == Key.Back) { - viewModel.onImeDismissed() - true - } else { - false + SelectedUserAwareInputConnection(selectedUserId) { + TextField( + value = password, + onValueChange = viewModel::onPasswordInputChanged, + enabled = isInputEnabled, + visualTransformation = PasswordVisualTransformation(), + singleLine = true, + textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = + KeyboardActions( + onDone = { viewModel.onAuthenticateKeyPressed() }, + ), + modifier = + modifier + .focusRequester(focusRequester) + .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) } + .drawBehind { + drawLine( + color = color, + start = Offset(x = 0f, y = size.height - lineWidthPx), + end = Offset(size.width, y = size.height - lineWidthPx), + strokeWidth = lineWidthPx, + ) } - }, - trailingIcon = - if (isImeSwitcherButtonVisible) { - { ImeSwitcherButton(viewModel, color) } - } else null - ) + .onInterceptKeyBeforeSoftKeyboard { keyEvent -> + if (keyEvent.key == Key.Back) { + viewModel.onImeDismissed() + true + } else { + false + } + }, + trailingIcon = + if (isImeSwitcherButtonVisible) { + { ImeSwitcherButton(viewModel, color) } + } else { + null + } + ) + } } /** Button for changing the password input method (IME). */ diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/SelectedUserAwareInputConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/SelectedUserAwareInputConnection.kt new file mode 100644 index 000000000000..c8e145034551 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/SelectedUserAwareInputConnection.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalComposeUiApi::class) + +package com.android.systemui.common.ui.compose + +import android.annotation.UserIdInt +import android.os.UserHandle +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.InterceptPlatformTextInput +import androidx.compose.ui.platform.PlatformTextInputMethodRequest + +/** + * Wrapper for input connection composables that need to be aware of the selected user to connect to + * the correct instance of on-device services like autofill, autocorrect, etc. + * + * Usage: + * ``` + * @Composable + * fun YourFunction(viewModel: YourViewModel) { + * val selectedUserId by viewModel.selectedUserId.collectAsState() + * + * SelectedUserAwareInputConnection(selectedUserId) { + * TextField(...) + * } + * } + * ``` + */ +@Composable +fun SelectedUserAwareInputConnection( + @UserIdInt selectedUserId: Int, + content: @Composable () -> Unit, +) { + InterceptPlatformTextInput( + interceptor = { request, nextHandler -> + // Create a new request to wrap the incoming one with some custom logic. + val modifiedRequest = + object : PlatformTextInputMethodRequest { + override fun createInputConnection(outAttributes: EditorInfo): InputConnection { + val inputConnection = request.createInputConnection(outAttributes) + // After the original request finishes initializing the EditorInfo we can + // customize it. If we needed to we could also wrap the InputConnection + // before + // returning it. + updateEditorInfo(outAttributes) + return inputConnection + } + + fun updateEditorInfo(outAttributes: EditorInfo) { + outAttributes.targetInputMethodUser = UserHandle.of(selectedUserId) + } + } + + // Send our wrapping request to the next handler, which could be the system or another + // interceptor up the tree. + nextHandler.startInputMethod(modifiedRequest) + } + ) { + content() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 92eb8f8c36c2..4950b96b077f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -45,32 +45,32 @@ import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayView import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor -import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.LockscreenShadeTransitionController -import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -102,6 +102,7 @@ private const val SENSOR_HEIGHT = 60 @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) class UdfpsControllerOverlayTest : SysuiTestCase() { + private val kosmos = testKosmos() @JvmField @Rule var rule = MockitoJUnit.rule() @@ -148,28 +149,11 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Before fun setup() { - testScope = TestScope(StandardTestDispatcher()) - powerRepository = FakePowerRepository() - powerInteractor = - PowerInteractor( - powerRepository, - mock(FalsingCollector::class.java), - mock(ScreenOffAnimationController::class.java), - statusBarStateController, - ) - keyguardTransitionRepository = FakeKeyguardTransitionRepository() - keyguardTransitionInteractor = - KeyguardTransitionInteractor( - scope = testScope.backgroundScope, - repository = keyguardTransitionRepository, - fromLockscreenTransitionInteractor = { - mock(FromLockscreenTransitionInteractor::class.java) - }, - fromPrimaryBouncerTransitionInteractor = { - mock(FromPrimaryBouncerTransitionInteractor::class.java) - }, - fromAodTransitionInteractor = { mock(FromAodTransitionInteractor::class.java) }, - ) + testScope = kosmos.testScope + powerRepository = kosmos.fakePowerRepository + powerInteractor = kosmos.powerInteractor + keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor whenever(inflater.inflate(R.layout.udfps_view, null, false)).thenReturn(udfpsView) whenever(inflater.inflate(R.layout.udfps_bp_view, null)) .thenReturn(mock(UdfpsBpView::class.java)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt index fb46ed9d54ed..b3380ff6409c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt @@ -25,18 +25,17 @@ import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest -import com.android.systemui.power.domain.interactor.PowerInteractorFactory +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LightRevealEffect +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -49,9 +48,10 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class LightRevealScrimRepositoryTest : SysuiTestCase() { - private lateinit var fakeKeyguardRepository: FakeKeyguardRepository - private lateinit var powerRepository: FakePowerRepository - private lateinit var powerInteractor: PowerInteractor + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository + private val powerInteractor = kosmos.powerInteractor private lateinit var underTest: LightRevealScrimRepositoryImpl @get:Rule val animatorTestRule = AnimatorTestRule(this) @@ -59,13 +59,13 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - fakeKeyguardRepository = FakeKeyguardRepository() - powerRepository = FakePowerRepository() - powerInteractor = - PowerInteractorFactory.create(repository = powerRepository).powerInteractor - underTest = - LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context, powerInteractor, mock()) + LightRevealScrimRepositoryImpl( + kosmos.fakeKeyguardRepository, + context, + kosmos.powerInteractor, + mock() + ) } @Test @@ -168,8 +168,9 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) fun revealAmount_emitsTo1AfterAnimationStarted() = - runTest(UnconfinedTestDispatcher()) { + testScope.runTest { val value by collectLastValue(underTest.revealAmount) + runCurrent() underTest.startRevealAmountAnimator(true) assertEquals(0.0f, value) animatorTestRule.advanceTimeBy(500L) @@ -179,8 +180,9 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) fun revealAmount_startingRevealTwiceWontRerunAnimator() = - runTest(UnconfinedTestDispatcher()) { + testScope.runTest { val value by collectLastValue(underTest.revealAmount) + runCurrent() underTest.startRevealAmountAnimator(true) assertEquals(0.0f, value) animatorTestRule.advanceTimeBy(250L) @@ -193,12 +195,14 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) fun revealAmount_emitsTo0AfterAnimationStartedReversed() = - runTest(UnconfinedTestDispatcher()) { - val value by collectLastValue(underTest.revealAmount) + testScope.runTest { + val lastValue by collectLastValue(underTest.revealAmount) + runCurrent() + underTest.startRevealAmountAnimator(true) + animatorTestRule.advanceTimeBy(500L) underTest.startRevealAmountAnimator(false) - assertEquals(1.0f, value) animatorTestRule.advanceTimeBy(500L) - assertEquals(0.0f, value) + assertEquals(0.0f, lastValue) } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index f9ec3d161bb0..24c651f3c702 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -18,9 +18,11 @@ package com.android.systemui.keyguard.domain.interactor import android.app.StatusBarManager +import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository @@ -120,6 +122,7 @@ class KeyguardInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testKeyguardGuardVisibilityStopsSecureCamera() = testScope.runTest { val secureCameraActive = collectLastValue(underTest.isSecureCameraActive) @@ -144,6 +147,7 @@ class KeyguardInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testBouncerShowingResetsSecureCameraState() = testScope.runTest { val secureCameraActive = collectLastValue(underTest.isSecureCameraActive) @@ -166,6 +170,7 @@ class KeyguardInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun keyguardVisibilityIsDefinedAsKeyguardShowingButNotOccluded() = runTest { val isVisible = collectLastValue(underTest.isKeyguardVisible) repository.setKeyguardShowing(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 63abc8f34668..0ebcf5608bff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.dock.DockManager import com.android.systemui.dock.DockManagerFake import com.android.systemui.flags.FakeFeatureFlags @@ -49,6 +50,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences @@ -57,6 +59,7 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -80,6 +83,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var launchAnimator: DialogTransitionAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var shadeInteractor: ShadeInteractor @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardQuickAffordanceInteractor @@ -179,6 +183,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = withDeps.keyguardInteractor, + shadeInteractor = shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, userTracker = userTracker, @@ -193,6 +198,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { backgroundDispatcher = testDispatcher, appContext = context, ) + + whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f)) } @Test @@ -339,6 +346,25 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @Test + fun quickAffordance_updateOncePerShadeExpansion() = + testScope.runTest { + val shadeExpansion = MutableStateFlow(0f) + whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion) + + val collectedValue by + collectValues( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + + val initialSize = collectedValue.size + for (i in 0..10) { + shadeExpansion.value = i / 10f + } + + assertThat(collectedValue.size).isEqualTo(initialSize + 1) + } + + @Test fun quickAffordanceAlwaysVisible_notVisible_restrictedByPolicyManager() = testScope.runTest { whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)) diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml index 61f69c04c174..f6042e467987 100644 --- a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml +++ b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml @@ -128,7 +128,8 @@ android:layout_marginStart="49dp" android:layout_marginEnd="49dp" android:overScrollMode="never" - android:layout_marginBottom="16dp"> + android:layout_marginBottom="16dp" + android:scrollbars="none"> <LinearLayout android:id="@+id/keyboard_shortcuts_container" android:layout_width="match_parent" diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 88535893b60c..33f14d44de75 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -22,17 +22,13 @@ import android.util.ArrayMap; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; -import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.Preconditions; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.accessibility.AccessibilityButtonModeObserver; -import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.fragments.FragmentService; @@ -47,7 +43,6 @@ import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationMediaManager; -import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.stack.AmbientState; @@ -58,7 +53,6 @@ import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.policy.FlashlightController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.tuner.TunablePadding.TunablePaddingService; import com.android.systemui.tuner.TunerService; @@ -70,6 +64,7 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; + /** * Class to handle ugly dependencies throughout sysui until we determine the * long-term dependency injection solution. @@ -96,10 +91,6 @@ public class Dependency { * Key for getting a Handler for receiving time tick broadcasts on. */ public static final String TIME_TICK_HANDLER_NAME = "time_tick_handler"; - /** - * Generic handler on the main thread. - */ - private static final String MAIN_HANDLER_NAME = "main_handler"; /** * An email address to send memory leak reports to by default. @@ -121,11 +112,6 @@ public class Dependency { */ public static final DependencyKey<Handler> TIME_TICK_HANDLER = new DependencyKey<>(TIME_TICK_HANDLER_NAME); - /** - * Generic handler on the main thread. - */ - public static final DependencyKey<Handler> MAIN_HANDLER = - new DependencyKey<>(MAIN_HANDLER_NAME); private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>(); private final ArrayMap<Object, LazyDependencyCreator> mProviders = new ArrayMap<>(); @@ -134,7 +120,6 @@ public class Dependency { @Inject Lazy<BroadcastDispatcher> mBroadcastDispatcher; @Inject Lazy<BluetoothController> mBluetoothController; - @Inject Lazy<FlashlightController> mFlashlightController; @Inject Lazy<KeyguardUpdateMonitor> mKeyguardUpdateMonitor; @Inject Lazy<DeviceProvisionedController> mDeviceProvisionedController; @Inject Lazy<PluginManager> mPluginManager; @@ -150,15 +135,10 @@ public class Dependency { @Inject Lazy<LightBarController> mLightBarController; @Inject Lazy<OverviewProxyService> mOverviewProxyService; @Inject Lazy<NavigationModeController> mNavBarModeController; - @Inject Lazy<AccessibilityButtonModeObserver> mAccessibilityButtonModeObserver; - @Inject Lazy<AccessibilityButtonTargetsObserver> mAccessibilityButtonListController; - @Inject Lazy<IStatusBarService> mIStatusBarService; - @Inject Lazy<NotificationRemoteInputManager.Callback> mNotificationRemoteInputManagerCallback; @Inject Lazy<NavigationBarController> mNavigationBarController; @Inject Lazy<StatusBarStateController> mStatusBarStateController; @Inject Lazy<NotificationMediaManager> mNotificationMediaManager; @Inject @Background Lazy<Looper> mBgLooper; - @Inject @Main Lazy<Handler> mMainHandler; @Inject @Named(TIME_TICK_HANDLER_NAME) Lazy<Handler> mTimeTickHandler; @Inject Lazy<SysUiState> mSysUiStateFlagsContainer; @Inject Lazy<CommandQueue> mCommandQueue; @@ -187,10 +167,8 @@ public class Dependency { // on imports. mProviders.put(TIME_TICK_HANDLER, mTimeTickHandler::get); mProviders.put(BG_LOOPER, mBgLooper::get); - mProviders.put(MAIN_HANDLER, mMainHandler::get); mProviders.put(BroadcastDispatcher.class, mBroadcastDispatcher::get); mProviders.put(BluetoothController.class, mBluetoothController::get); - mProviders.put(FlashlightController.class, mFlashlightController::get); mProviders.put(KeyguardUpdateMonitor.class, mKeyguardUpdateMonitor::get); mProviders.put(DeviceProvisionedController.class, mDeviceProvisionedController::get); mProviders.put(PluginManager.class, mPluginManager::get); @@ -205,13 +183,6 @@ public class Dependency { mProviders.put(LightBarController.class, mLightBarController::get); mProviders.put(OverviewProxyService.class, mOverviewProxyService::get); mProviders.put(NavigationModeController.class, mNavBarModeController::get); - mProviders.put(AccessibilityButtonModeObserver.class, - mAccessibilityButtonModeObserver::get); - mProviders.put(AccessibilityButtonTargetsObserver.class, - mAccessibilityButtonListController::get); - mProviders.put(IStatusBarService.class, mIStatusBarService::get); - mProviders.put(NotificationRemoteInputManager.Callback.class, - mNotificationRemoteInputManagerCallback::get); mProviders.put(NavigationBarController.class, mNavigationBarController::get); mProviders.put(StatusBarStateController.class, mStatusBarStateController::get); mProviders.put(NotificationMediaManager.class, mNotificationMediaManager::get); diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index 1c8b84d82a56..b42eda108d54 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -73,6 +73,14 @@ class PasswordBouncerViewModel( initialValue = isInputEnabled.value && !isTextFieldFocused.value, ) + /** The ID of the currently-selected user. */ + val selectedUserId: StateFlow<Int> = + selectedUserInteractor.selectedUser.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = selectedUserInteractor.getSelectedUserId(), + ) + override fun onHidden() { super.onHidden() isTextFieldFocused.value = false diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt index 79455ebb624c..289dbd9f66e0 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt @@ -24,9 +24,12 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.model.BiometricMessage import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.domain.interactor.PowerInteractor @@ -60,17 +63,23 @@ constructor( private val context: Context, activityStarter: ActivityStarter, powerInteractor: PowerInteractor, + keyguardTransitionInteractor: KeyguardTransitionInteractor, ) { private val keyguardOccludedByApp: Flow<Boolean> = - combine( - keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isKeyguardShowing, - primaryBouncerInteractor.isShowing, - alternateBouncerInteractor.isVisible, - ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible -> - occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible - } - .distinctUntilChanged() + if (KeyguardWmStateRefactor.isEnabled) { + keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.OCCLUDED } + } else { + combine( + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.isKeyguardShowing, + primaryBouncerInteractor.isShowing, + alternateBouncerInteractor.isVisible, + ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible -> + occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible + } + .distinctUntilChanged() + } + private val fingerprintUnlockSuccessEvents: Flow<Unit> = fingerprintAuthRepository.authenticationStatus .ifKeyguardOccludedByApp() diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt index 931a86938592..ed82278a7346 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt @@ -163,6 +163,6 @@ constructor( } companion object { - const val KEY_UP_TIMEOUT = 100L + const val KEY_UP_TIMEOUT = 60L } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 2e233d8e5dd2..3134e35a92e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -289,6 +289,8 @@ public class KeyguardService extends Service { }; } + private final WindowManagerOcclusionManager mWmOcclusionManager; + @Inject public KeyguardService( KeyguardViewMediator keyguardViewMediator, @@ -302,7 +304,8 @@ public class KeyguardService extends Service { KeyguardSurfaceBehindParamsApplier keyguardSurfaceBehindAnimator, @Application CoroutineScope scope, FeatureFlags featureFlags, - PowerInteractor powerInteractor) { + PowerInteractor powerInteractor, + WindowManagerOcclusionManager windowManagerOcclusionManager) { super(); mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; @@ -323,6 +326,8 @@ public class KeyguardService extends Service { keyguardSurfaceBehindAnimator, scope); } + + mWmOcclusionManager = windowManagerOcclusionManager; } @Override @@ -414,7 +419,11 @@ public class KeyguardService extends Service { Trace.beginSection("KeyguardService.mBinder#setOccluded"); checkPermission(); - mKeyguardViewMediator.setOccluded(isOccluded, animate); + if (!KeyguardWmStateRefactor.isEnabled()) { + mKeyguardViewMediator.setOccluded(isOccluded, animate); + } else { + mWmOcclusionManager.onKeyguardServiceSetOccluded(isOccluded); + } Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 6d917bbde82b..a37397db81f8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -1365,7 +1365,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager; + private WindowManagerOcclusionManager mWmOcclusionManager; /** + * Injected constructor. See {@link KeyguardModule}. */ public KeyguardViewMediator( @@ -1411,7 +1413,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, SystemPropertiesHelper systemPropertiesHelper, Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager, SelectedUserInteractor selectedUserInteractor, - KeyguardInteractor keyguardInteractor) { + KeyguardInteractor keyguardInteractor, + WindowManagerOcclusionManager wmOcclusionManager) { mContext = context; mUserTracker = userTracker; mFalsingCollector = falsingCollector; @@ -1486,6 +1489,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard"); mShowKeyguardWakeLock.setReferenceCounted(false); + + mWmOcclusionManager = wmOcclusionManager; } public void userActivity() { @@ -2103,15 +2108,27 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } public IRemoteAnimationRunner getOccludeAnimationRunner() { - return validatingRemoteAnimationRunner(mOccludeAnimationRunner); + if (KeyguardWmStateRefactor.isEnabled()) { + return validatingRemoteAnimationRunner(mWmOcclusionManager.getOccludeAnimationRunner()); + } else { + return validatingRemoteAnimationRunner(mOccludeAnimationRunner); + } } + /** + * TODO(b/326464548): Move this to WindowManagerOcclusionManager + */ public IRemoteAnimationRunner getOccludeByDreamAnimationRunner() { return validatingRemoteAnimationRunner(mOccludeByDreamAnimationRunner); } public IRemoteAnimationRunner getUnoccludeAnimationRunner() { - return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner); + if (KeyguardWmStateRefactor.isEnabled()) { + return validatingRemoteAnimationRunner( + mWmOcclusionManager.getUnoccludeAnimationRunner()); + } else { + return validatingRemoteAnimationRunner(mUnoccludeAnimationRunner); + } } public boolean isHiding() { @@ -2145,8 +2162,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (mOccluded != isOccluded) { mOccluded = isOccluded; - mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate - && mDeviceInteractive); + if (!KeyguardWmStateRefactor.isEnabled()) { + mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate + && mDeviceInteractive); + } adjustStatusBarLocked(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt index 8ebcece940c2..00f50023b263 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt @@ -140,8 +140,14 @@ constructor( nonApps: Array<RemoteAnimationTarget>, finishedCallback: IRemoteAnimationFinishedCallback ) { - goingAwayRemoteAnimationFinishedCallback = finishedCallback - keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0]) + if (apps.isNotEmpty()) { + goingAwayRemoteAnimationFinishedCallback = finishedCallback + keyguardSurfaceBehindAnimator.applyParamsToSurface(apps[0]) + } else { + // Nothing to do here if we have no apps, end the animation, which will cancel it and WM + // will make *something* visible. + finishedCallback.onAnimationFinished() + } } fun onKeyguardGoingAwayRemoteAnimationCancelled() { @@ -174,13 +180,19 @@ constructor( * if so, true should be the right choice. */ private fun setWmLockscreenState( - lockscreenShowing: Boolean = this.isLockscreenShowing ?: true.also { - Log.d(TAG, "Using isLockscreenShowing=true default in setWmLockscreenState, " + - "because setAodVisible was called before the first setLockscreenShown " + - "call during boot. This is not typical, but is theoretically possible. " + - "If you're investigating the lockscreen showing unexpectedly, start here.") - }, - aodVisible: Boolean = this.isAodVisible + lockscreenShowing: Boolean = + this.isLockscreenShowing + ?: true.also { + Log.d( + TAG, + "Using isLockscreenShowing=true default in setWmLockscreenState, " + + "because setAodVisible was called before the first " + + "setLockscreenShown call during boot. This is not typical, but is " + + "theoretically possible. If you're investigating the lockscreen " + + "showing unexpectedly, start here." + ) + }, + aodVisible: Boolean = this.isAodVisible ) { Log.d( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt new file mode 100644 index 000000000000..aab90c378a19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerOcclusionManager.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Matrix +import android.os.RemoteException +import android.util.Log +import android.view.IRemoteAnimationFinishedCallback +import android.view.IRemoteAnimationRunner +import android.view.RemoteAnimationTarget +import android.view.SyncRtSurfaceTransactionApplier +import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED +import androidx.annotation.VisibleForTesting +import com.android.app.animation.Interpolators +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.policy.ScreenDecorationsUtils +import com.android.keyguard.KeyguardViewController +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.TransitionAnimator +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.res.R +import java.util.concurrent.Executor +import javax.inject.Inject + +private val UNOCCLUDE_ANIMATION_DURATION = 250 +private val UNOCCLUDE_TRANSLATE_DISTANCE_PERCENT = 0.1f + +/** + * Keeps track of Window Manager's occlusion state and RemoteAnimations related to changes in + * occlusion state. Occlusion is when a [FLAG_SHOW_WHEN_LOCKED] activity is displaying over the + * lockscreen - we're still locked, but the user can interact with the activity. + * + * Typical "occlusion" use cases include launching the camera over the lockscreen, tapping a quick + * affordance to bring up Google Pay/Wallet/whatever it's called by the time you're reading this, + * and Maps Navigation. + * + * Window Manager considers the keyguard to be 'occluded' whenever a [FLAG_SHOW_WHEN_LOCKED] + * activity is on top of the task stack, even if the device is unlocked and the keyguard is not + * visible. System UI considers the keyguard to be [KeyguardState.OCCLUDED] only when we're on the + * keyguard and an activity is displaying over it. + * + * For all System UI use cases, you should use [KeyguardTransitionInteractor] to determine if we're + * in the [KeyguardState.OCCLUDED] state and react accordingly. If you are sure that you need to + * check whether Window Manager considers OCCLUDED=true even though the lockscreen is not showing, + * use [KeyguardShowWhenLockedActivityInteractor.isShowWhenLockedActivityOnTop] in combination with + * [KeyguardTransitionInteractor] state. + * + * This is a very sensitive piece of state that has caused many headaches in the past. Please be + * careful. + */ +@SysUISingleton +class WindowManagerOcclusionManager +@Inject +constructor( + val keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + val activityTransitionAnimator: ActivityTransitionAnimator, + val keyguardViewController: dagger.Lazy<KeyguardViewController>, + val powerInteractor: PowerInteractor, + val context: Context, + val interactionJankMonitor: InteractionJankMonitor, + @Main executor: Executor, + val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, + val occlusionInteractor: KeyguardOcclusionInteractor, +) { + val powerButtonY = + context.resources.getDimensionPixelSize( + R.dimen.physical_power_button_center_screen_location_y + ) + val windowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + + var occludeAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null + + /** + * Animation runner provided to WindowManager, which will be used if an occluding activity is + * launched and Window Manager wants us to animate it in. This is used as a signal that we are + * now occluded, and should update our state accordingly. + */ + val occludeAnimationRunner: IRemoteAnimationRunner = + object : IRemoteAnimationRunner.Stub() { + override fun onAnimationStart( + transit: Int, + apps: Array<RemoteAnimationTarget>, + wallpapers: Array<RemoteAnimationTarget>, + nonApps: Array<RemoteAnimationTarget>, + finishedCallback: IRemoteAnimationFinishedCallback? + ) { + Log.d(TAG, "occludeAnimationRunner#onAnimationStart") + // Wrap the callback so that it's guaranteed to be nulled out once called. + occludeAnimationFinishedCallback = + object : IRemoteAnimationFinishedCallback.Stub() { + override fun onAnimationFinished() { + finishedCallback?.onAnimationFinished() + occludeAnimationFinishedCallback = null + } + } + keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( + showWhenLockedActivityOnTop = true, + taskInfo = apps.firstOrNull()?.taskInfo, + ) + activityTransitionAnimator + .createRunner(occludeAnimationController) + .onAnimationStart( + transit, + apps, + wallpapers, + nonApps, + occludeAnimationFinishedCallback, + ) + } + + override fun onAnimationCancelled() { + Log.d(TAG, "occludeAnimationRunner#onAnimationCancelled") + } + } + + var unoccludeAnimationFinishedCallback: IRemoteAnimationFinishedCallback? = null + + /** + * Animation runner provided to WindowManager, which will be used if an occluding activity is + * finished and Window Manager wants us to animate it out. This is used as a signal that we are + * no longer occluded, and should update our state accordingly. + * + * TODO(b/326464548): Restore dream specific animation. + */ + val unoccludeAnimationRunner: IRemoteAnimationRunner = + object : IRemoteAnimationRunner.Stub() { + var unoccludeAnimator: ValueAnimator? = null + val unoccludeMatrix = Matrix() + + /** TODO(b/326470033): Extract this logic into ViewModels. */ + override fun onAnimationStart( + transit: Int, + apps: Array<RemoteAnimationTarget>, + wallpapers: Array<RemoteAnimationTarget>, + nonApps: Array<RemoteAnimationTarget>, + finishedCallback: IRemoteAnimationFinishedCallback? + ) { + Log.d(TAG, "unoccludeAnimationRunner#onAnimationStart") + // Wrap the callback so that it's guaranteed to be nulled out once called. + unoccludeAnimationFinishedCallback = + object : IRemoteAnimationFinishedCallback.Stub() { + override fun onAnimationFinished() { + finishedCallback?.onAnimationFinished() + unoccludeAnimationFinishedCallback = null + } + } + keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( + showWhenLockedActivityOnTop = false, + taskInfo = apps.firstOrNull()?.taskInfo, + ) + interactionJankMonitor.begin( + createInteractionJankMonitorConf( + InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION, + "UNOCCLUDE" + ) + ) + if (apps.isEmpty()) { + Log.d( + TAG, + "No apps provided to unocclude runner; " + + "skipping animation and unoccluding." + ) + unoccludeAnimationFinishedCallback?.onAnimationFinished() + return + } + val target = apps[0] + val localView: View = keyguardViewController.get().getViewRootImpl().getView() + val applier = SyncRtSurfaceTransactionApplier(localView) + // TODO( + executor.execute { + unoccludeAnimator?.cancel() + unoccludeAnimator = + ValueAnimator.ofFloat(1f, 0f).apply { + duration = UNOCCLUDE_ANIMATION_DURATION.toLong() + interpolator = Interpolators.TOUCH_RESPONSE + addUpdateListener { animation: ValueAnimator -> + val animatedValue = animation.animatedValue as Float + val surfaceHeight: Float = + target.screenSpaceBounds.height().toFloat() + + unoccludeMatrix.setTranslate( + 0f, + (1f - animatedValue) * + surfaceHeight * + UNOCCLUDE_TRANSLATE_DISTANCE_PERCENT + ) + + SurfaceParams.Builder(target.leash) + .withAlpha(animatedValue) + .withMatrix(unoccludeMatrix) + .withCornerRadius(windowCornerRadius) + .build() + .also { applier.scheduleApply(it) } + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + try { + unoccludeAnimationFinishedCallback + ?.onAnimationFinished() + unoccludeAnimator = null + interactionJankMonitor.end( + InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION + ) + } catch (e: RemoteException) { + e.printStackTrace() + } + } + } + ) + start() + } + } + } + + override fun onAnimationCancelled() { + Log.d(TAG, "unoccludeAnimationRunner#onAnimationCancelled") + context.mainExecutor.execute { unoccludeAnimator?.cancel() } + Log.d(TAG, "Unocclude animation cancelled.") + interactionJankMonitor.cancel(InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION) + } + } + + /** + * Called when Window Manager tells the KeyguardService directly that we're occluded or not + * occluded, without starting an occlude/unocclude remote animation. This happens if occlusion + * state changes without an animation (such as if a SHOW_WHEN_LOCKED activity is launched while + * we're unlocked), or if an animation has been cancelled/interrupted and Window Manager wants + * to make sure that we're in the correct state. + */ + fun onKeyguardServiceSetOccluded(occluded: Boolean) { + Log.d(TAG, "#onKeyguardServiceSetOccluded($occluded)") + keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(occluded) + } + + @VisibleForTesting + val occludeAnimationController: ActivityTransitionAnimator.Controller = + object : ActivityTransitionAnimator.Controller { + + override var transitionContainer: ViewGroup + get() = keyguardViewController.get().getViewRootImpl().view as ViewGroup + set(_) { + // Should never be set. + } + + /** TODO(b/326470033): Extract this logic into ViewModels. */ + override fun createAnimatorState(): TransitionAnimator.State { + val fullWidth = transitionContainer.width + val fullHeight = transitionContainer.height + + if ( + keyguardOcclusionInteractor.showWhenLockedActivityLaunchedFromPowerGesture.value + ) { + val initialHeight = fullHeight / 3f + val initialWidth = fullWidth / 3f + + // Start the animation near the power button, at one-third size, since the + // camera was launched from the power button. + return TransitionAnimator.State( + top = (powerButtonY - initialHeight / 2f).toInt(), + bottom = (powerButtonY + initialHeight / 2f).toInt(), + left = (fullWidth - initialWidth).toInt(), + right = fullWidth, + topCornerRadius = windowCornerRadius, + bottomCornerRadius = windowCornerRadius, + ) + } else { + val initialHeight = fullHeight / 2f + val initialWidth = fullWidth / 2f + + // Start the animation in the center of the screen, scaled down to half + // size. + return TransitionAnimator.State( + top = (fullHeight - initialHeight).toInt() / 2, + bottom = (initialHeight + (fullHeight - initialHeight) / 2).toInt(), + left = (fullWidth - initialWidth).toInt() / 2, + right = (initialWidth + (fullWidth - initialWidth) / 2).toInt(), + topCornerRadius = windowCornerRadius, + bottomCornerRadius = windowCornerRadius, + ) + } + } + } + + private fun createInteractionJankMonitorConf( + cuj: Int, + tag: String? + ): InteractionJankMonitor.Configuration.Builder { + val builder = + InteractionJankMonitor.Configuration.Builder.withView( + cuj, + keyguardViewController.get().getViewRootImpl().view + ) + return if (tag != null) builder.setTag(tag) else builder + } + + companion object { + val TAG = "WindowManagerOcclusion" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 968c3e3a6792..5306645bf69f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -50,6 +50,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager; +import com.android.systemui.keyguard.WindowManagerOcclusionManager; import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; @@ -163,7 +164,8 @@ public interface KeyguardModule { SystemPropertiesHelper systemPropertiesHelper, Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager, SelectedUserInteractor selectedUserInteractor, - KeyguardInteractor keyguardInteractor) { + KeyguardInteractor keyguardInteractor, + WindowManagerOcclusionManager windowManagerOcclusionManager) { return new KeyguardViewMediator( context, uiEventLogger, @@ -209,7 +211,8 @@ public interface KeyguardModule { systemPropertiesHelper, wmLockscreenVisibilityManager, selectedUserInteractor, - keyguardInteractor); + keyguardInteractor, + windowManagerOcclusionManager); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt new file mode 100644 index 000000000000..e3654b415017 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepository.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Information about the SHOW_WHEN_LOCKED activity that is either newly on top of the task stack, or + * newly not on top of the task stack. + */ +data class ShowWhenLockedActivityInfo( + /** Whether the activity is on top. If not, we're unoccluding and will be animating it out. */ + val isOnTop: Boolean, + + /** + * Information about the activity, which we use for transition internals and also to customize + * animations. + */ + val taskInfo: RunningTaskInfo? = null +) { + fun isDream(): Boolean { + return taskInfo?.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM + } +} + +/** + * Maintains state about "occluding" activities - activities with FLAG_SHOW_WHEN_LOCKED, which are + * capable of displaying over the lockscreen while the device is still locked (such as Google Maps + * navigation). + * + * Window Manager considers the device to be in the "occluded" state whenever such an activity is on + * top of the task stack, including while we're unlocked, while keyguard code considers us to be + * occluded only when we're locked, with an occluding activity currently displaying over the + * lockscreen. + * + * This dual definition is confusing, so this repository collects all of the signals WM gives us, + * and consolidates them into [showWhenLockedActivityInfo.isOnTop], which is the actual question WM + * is answering when they say whether we're 'occluded'. Keyguard then uses this signal to + * conditionally transition to [KeyguardState.OCCLUDED] where appropriate. + */ +@SysUISingleton +class KeyguardOcclusionRepository @Inject constructor() { + val showWhenLockedActivityInfo = MutableStateFlow(ShowWhenLockedActivityInfo(isOnTop = false)) + + /** + * Sets whether there's a SHOW_WHEN_LOCKED activity on top of the task stack, and optionally, + * information about the activity itself. + * + * If no value is provided for [taskInfo], we'll default to the current [taskInfo]. + * + * The [taskInfo] is always present when this method is called from the occlude/unocclude + * animation runners. We use the default when calling from [KeyguardService.isOccluded], since + * we only receive a true/false value there. isOccluded is mostly redundant - it's almost always + * called with true after an occlusion animation has started, and with false after an unocclude + * animation has started. In those cases, we don't want to clear out the taskInfo just because + * it wasn't available at that call site. + */ + fun setShowWhenLockedActivityInfo( + onTop: Boolean, + taskInfo: RunningTaskInfo? = showWhenLockedActivityInfo.value.taskInfo + ) { + showWhenLockedActivityInfo.value = + ShowWhenLockedActivityInfo( + isOnTop = onTop, + taskInfo = taskInfo, + ) + } +} 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 64e28700aa74..3a6423d680c6 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 @@ -95,6 +95,7 @@ interface KeyguardRepository { val isKeyguardShowing: Flow<Boolean> /** Is an activity showing over the keyguard? */ + @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED") val isKeyguardOccluded: Flow<Boolean> /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index 9b3f13d16911..d9479de7e25b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -75,7 +75,6 @@ constructor( powerInteractor: PowerInteractor, private val scrimLogger: ScrimLogger, ) : LightRevealScrimRepository { - companion object { val TAG = LightRevealScrimRepository::class.simpleName!! } @@ -156,7 +155,11 @@ constructor( override fun startRevealAmountAnimator(reveal: Boolean) { if (reveal == willBeOrIsRevealed) return willBeOrIsRevealed = reveal - if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse() + if (reveal && !revealAmountAnimator.isRunning) { + revealAmountAnimator.start() + } else { + revealAmountAnimator.reverse() + } scrimLogger.d(TAG, "startRevealAmountAnimator, reveal: ", reveal) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt index 6aed944ac809..88ddfd4f4347 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor @@ -46,13 +47,16 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, private val communalInteractor: CommunalInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.ALTERNATE_BOUNCER, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { @@ -112,6 +116,11 @@ constructor( } private fun listenForAlternateBouncerToGone() { + if (KeyguardWmStateRefactor.isEnabled) { + // Handled via #dismissAlternateBouncer. + return + } + scope.launch { keyguardInteractor.isKeyguardGoingAway .sampleUtil(finishedKeyguardState, ::Pair) @@ -149,6 +158,10 @@ constructor( } } + fun dismissAlternateBouncer() { + scope.launch { startTransitionTo(KeyguardState.GONE) } + } + companion object { const val TAG = "FromAlternateBouncerTransitionInteractor" val TRANSITION_DURATION_MS = 300.milliseconds diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index dbd5e26eacfc..9040e031d54e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -27,14 +27,17 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.sample +import java.util.UUID import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @SysUISingleton @@ -47,29 +50,118 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.AOD, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { - listenForAodToLockscreen() + listenForAodToAwake() + listenForAodToOccluded() listenForAodToPrimaryBouncer() listenForAodToGone() - listenForAodToOccluded() listenForTransitionToCamera(scope, keyguardInteractor) } /** + * Listen for the signal that we're waking up and figure what state we need to transition to. + */ + private fun listenForAodToAwake() { + val transitionToLockscreen: suspend (TransitionStep) -> UUID? = + { startedStep: TransitionStep -> + val modeOnCanceled = + if (startedStep.from == KeyguardState.LOCKSCREEN) { + TransitionModeOnCanceled.REVERSE + } else if (startedStep.from == KeyguardState.GONE) { + TransitionModeOnCanceled.RESET + } else { + TransitionModeOnCanceled.LAST_VALUE + } + startTransitionTo( + toState = KeyguardState.LOCKSCREEN, + modeOnCanceled = modeOnCanceled, + ) + } + + if (KeyguardWmStateRefactor.isEnabled) { + // The refactor uses PowerInteractor's wakefulness, which is the earliest wake signal + // available. We have all of the information we need at this time to make a decision + // about where to transition. + scope.launch { + powerInteractor.detailedWakefulness + // React only to wake events. + .filter { it.isAwake() } + .sample( + startedKeyguardTransitionStep, + keyguardInteractor.biometricUnlockState, + keyguardInteractor.primaryBouncerShowing, + ) + // Make sure we've at least STARTED a transition to AOD. + .filter { (_, startedStep, _, _) -> startedStep.to == KeyguardState.AOD } + .collect { (_, startedStep, biometricUnlockState, primaryBouncerShowing) -> + // Check with the superclass to see if an occlusion transition is needed. + // Also, don't react to wake and unlock events, as we'll be receiving a call + // to #dismissAod() shortly when the authentication completes. + if ( + !maybeStartTransitionToOccludedOrInsecureCamera() && + !isWakeAndUnlock(biometricUnlockState) && + !primaryBouncerShowing + ) { + transitionToLockscreen(startedStep) + } + } + } + } else { + scope.launch { + keyguardInteractor + .dozeTransitionTo(DozeStateModel.FINISH) + .sample( + keyguardInteractor.isKeyguardShowing, + startedKeyguardTransitionStep, + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.biometricUnlockState, + keyguardInteractor.primaryBouncerShowing, + ) + .collect { + ( + _, + isKeyguardShowing, + lastStartedStep, + occluded, + biometricUnlockState, + primaryBouncerShowing) -> + if ( + lastStartedStep.to == KeyguardState.AOD && + !occluded && + !isWakeAndUnlock(biometricUnlockState) && + isKeyguardShowing && + !primaryBouncerShowing + ) { + transitionToLockscreen(lastStartedStep) + } + } + } + } + } + + /** * There are cases where the transition to AOD begins but never completes, such as tapping power * during an incoming phone call when unlocked. In this case, GONE->AOD should be interrupted to * run AOD->OCCLUDED. */ private fun listenForAodToOccluded() { + if (KeyguardWmStateRefactor.isEnabled) { + // Handled by calls to maybeStartTransitionToOccludedOrInsecureCamera on waking. + return + } + scope.launch { keyguardInteractor.isKeyguardOccluded .sample(startedKeyguardTransitionStep, ::Pair) @@ -84,49 +176,6 @@ constructor( } } - private fun listenForAodToLockscreen() { - scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.FINISH) - .sample( - keyguardInteractor.isKeyguardShowing, - startedKeyguardTransitionStep, - keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.biometricUnlockState, - keyguardInteractor.primaryBouncerShowing, - ) - .collect { - ( - _, - isKeyguardShowing, - lastStartedStep, - occluded, - biometricUnlockState, - primaryBouncerShowing) -> - if ( - lastStartedStep.to == KeyguardState.AOD && - !occluded && - !isWakeAndUnlock(biometricUnlockState) && - isKeyguardShowing && - !primaryBouncerShowing - ) { - val modeOnCanceled = - if (lastStartedStep.from == KeyguardState.LOCKSCREEN) { - TransitionModeOnCanceled.REVERSE - } else if (lastStartedStep.from == KeyguardState.GONE) { - TransitionModeOnCanceled.RESET - } else { - TransitionModeOnCanceled.LAST_VALUE - } - startTransitionTo( - toState = KeyguardState.LOCKSCREEN, - modeOnCanceled = modeOnCanceled, - ) - } - } - } - } - /** * If there is a biometric lockout and FPS is tapped while on AOD, it should go directly to the * PRIMARY_BOUNCER. @@ -145,6 +194,7 @@ constructor( private fun listenForAodToGone() { if (KeyguardWmStateRefactor.isEnabled) { + // Handled via #dismissAod. return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 8591fe782d3a..57b2a632008a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState @@ -34,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @SysUISingleton @@ -46,18 +48,22 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.DOZING, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { listenForDozingToAny() + listenForWakeFromDozing() listenForTransitionToCamera(scope, keyguardInteractor) } @@ -70,6 +76,10 @@ constructor( } private fun listenForDozingToAny() { + if (KeyguardWmStateRefactor.isEnabled) { + return + } + scope.launch { powerInteractor.isAwake .debounce(50L) @@ -112,6 +122,58 @@ constructor( } } + /** Figure out what state to transition to when we awake from DOZING. */ + private fun listenForWakeFromDozing() { + if (!KeyguardWmStateRefactor.isEnabled) { + return + } + + scope.launch { + powerInteractor.detailedWakefulness + .filter { it.isAwake() } + .sample( + startedKeyguardTransitionStep, + communalInteractor.isIdleOnCommunal, + keyguardInteractor.biometricUnlockState, + canDismissLockScreen, + keyguardInteractor.primaryBouncerShowing, + ) + // If we haven't at least STARTED a transition to DOZING, ignore. + .filter { (_, startedStep, _, _) -> startedStep.to == KeyguardState.DOZING } + .collect { + ( + _, + _, + isIdleOnCommunal, + biometricUnlockState, + canDismissLockscreen, + primaryBouncerShowing) -> + if ( + !maybeStartTransitionToOccludedOrInsecureCamera() && + // Handled by dismissFromDozing(). + !isWakeAndUnlock(biometricUnlockState) + ) { + startTransitionTo( + if (canDismissLockscreen) { + KeyguardState.GONE + } else if (primaryBouncerShowing) { + KeyguardState.PRIMARY_BOUNCER + } else if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + ) + } + } + } + } + + /** Dismisses keyguard from the DOZING state. */ + fun dismissFromDozing() { + scope.launch { startTransitionTo(KeyguardState.GONE) } + } + override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { return ValueAnimator().apply { interpolator = Interpolators.LINEAR diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt index a6cdaa8c6761..6433d0ede796 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -46,12 +47,16 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, + powerInteractor: PowerInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.DREAMING_LOCKSCREEN_HOSTED, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index acfa107cc1f1..31371384e338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -23,10 +23,14 @@ import com.android.systemui.Flags.communalHub import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.Utils +import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -34,6 +38,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @SysUISingleton @@ -47,17 +52,23 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, private val glanceableHubTransitions: GlanceableHubTransitions, + powerInteractor: PowerInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.DREAMING, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { listenForDreamingToOccluded() - listenForDreamingToGone() + listenForDreamingToGoneWhenDismissable() + listenForDreamingToGoneFromBiometricUnlock() + listenForDreamingToLockscreen() listenForDreamingToAodOrDozing() listenForTransitionToCamera(scope, keyguardInteractor) listenForDreamingToGlanceableHub() @@ -76,6 +87,7 @@ constructor( fun startToLockscreenTransition() { scope.launch { + KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode() if ( transitionInteractor.startedKeyguardState.replayCache.last() == KeyguardState.DREAMING @@ -86,22 +98,80 @@ constructor( } private fun listenForDreamingToOccluded() { + if (KeyguardWmStateRefactor.isEnabled) { + scope.launch { + combine( + keyguardInteractor.isDreaming, + keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop, + ::Pair + ) + .sample(startedKeyguardTransitionStep, ::toTriple) + .filter { (isDreaming, _, startedStep) -> + !isDreaming && startedStep.to == KeyguardState.DREAMING + } + .collect { maybeStartTransitionToOccludedOrInsecureCamera() } + } + } else { + scope.launch { + combine( + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.isDreaming, + ::Pair + ) + .sample(startedKeyguardTransitionStep, Utils.Companion::toTriple) + .collect { (isOccluded, isDreaming, lastStartedTransition) -> + if ( + isOccluded && + !isDreaming && + lastStartedTransition.to == KeyguardState.DREAMING + ) { + startTransitionTo(KeyguardState.OCCLUDED) + } + } + } + } + } + + private fun listenForDreamingToLockscreen() { + if (!KeyguardWmStateRefactor.isEnabled) { + return + } + + scope.launch { + keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop + .filter { onTop -> !onTop } + .sample(startedKeyguardState) + .collect { startedState -> + if (startedState == KeyguardState.DREAMING) { + startTransitionTo(KeyguardState.LOCKSCREEN) + } + } + } + } + + private fun listenForDreamingToGoneWhenDismissable() { scope.launch { - combine(keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isDreaming, ::Pair) - .sample(startedKeyguardTransitionStep, ::toTriple) - .collect { (isOccluded, isDreaming, lastStartedTransition) -> + keyguardInteractor.isAbleToDream + .sampleCombine( + keyguardInteractor.isKeyguardShowing, + keyguardInteractor.isKeyguardDismissible, + startedKeyguardTransitionStep, + ) + .collect { + (isDreaming, isKeyguardShowing, isKeyguardDismissible, lastStartedTransition) -> if ( - isOccluded && - !isDreaming && - lastStartedTransition.to == KeyguardState.DREAMING + !isDreaming && + lastStartedTransition.to == KeyguardState.DREAMING && + isKeyguardDismissible && + !isKeyguardShowing ) { - startTransitionTo(KeyguardState.OCCLUDED) + startTransitionTo(KeyguardState.GONE) } } } } - private fun listenForDreamingToGone() { + private fun listenForDreamingToGoneFromBiometricUnlock() { scope.launch { keyguardInteractor.biometricUnlockState .sample(startedKeyguardTransitionStep, ::Pair) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 786c3c6697d9..51bc3ae778e5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -23,6 +23,7 @@ import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled @@ -35,6 +36,7 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -49,14 +51,18 @@ constructor( private val keyguardInteractor: KeyguardInteractor, override val transitionRepository: KeyguardTransitionRepository, transitionInteractor: KeyguardTransitionInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.GLANCEABLE_HUB, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { + override fun start() { if (!Flags.communalHub()) { return @@ -151,14 +157,27 @@ constructor( } private fun listenForHubToOccluded() { - scope.launch { - and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming)) - .sample(startedKeyguardState, ::Pair) - .collect { (isOccludedAndNotDreaming, keyguardState) -> - if (isOccludedAndNotDreaming && keyguardState == fromState) { - startTransitionTo(KeyguardState.OCCLUDED) + if (KeyguardWmStateRefactor.isEnabled) { + scope.launch { + keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop + .filter { onTop -> onTop } + .sample(startedKeyguardState) + .collect { + if (it == KeyguardState.GLANCEABLE_HUB) { + maybeStartTransitionToOccludedOrInsecureCamera() + } } - } + } + } else { + scope.launch { + and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming)) + .sample(startedKeyguardState, ::Pair) + .collect { (isOccludedAndNotDreaming, keyguardState) -> + if (isOccludedAndNotDreaming && keyguardState == fromState) { + startTransitionTo(KeyguardState.OCCLUDED) + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 7593ac252543..d5a9bd19d766 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -22,6 +22,8 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardWmStateRefactor +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled @@ -34,6 +36,8 @@ import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @SysUISingleton @@ -46,14 +50,18 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + private val biometricSettingsRepository: BiometricSettingsRepository, ) : TransitionInteractor( fromState = KeyguardState.GONE, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { @@ -65,23 +73,46 @@ constructor( // Primarily for when the user chooses to lock down the device private fun listenForGoneToLockscreenOrHub() { - scope.launch { - keyguardInteractor.isKeyguardShowing - .sample( - startedKeyguardState, - communalInteractor.isIdleOnCommunal, - ) - .collect { (isKeyguardShowing, startedState, isIdleOnCommunal) -> - if (isKeyguardShowing && startedState == KeyguardState.GONE) { - val to = - if (isIdleOnCommunal) { - KeyguardState.GLANCEABLE_HUB - } else { - KeyguardState.LOCKSCREEN - } - startTransitionTo(to) + if (KeyguardWmStateRefactor.isEnabled) { + scope.launch { + biometricSettingsRepository.isCurrentUserInLockdown + .distinctUntilChanged() + .filter { inLockdown -> inLockdown } + .sample( + startedKeyguardState, + communalInteractor.isIdleOnCommunal, + ) + .collect { (_, startedState, isIdleOnCommunal) -> + if (startedState == KeyguardState.GONE) { + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to, ownerReason = "User initiated lockdown") + } } - } + } + } else { + scope.launch { + keyguardInteractor.isKeyguardShowing + .sample( + startedKeyguardState, + communalInteractor.isIdleOnCommunal, + ) + .collect { (isKeyguardShowing, startedState, isIdleOnCommunal) -> + if (isKeyguardShowing && startedState == KeyguardState.GONE) { + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to) + } + } + } } } @@ -122,24 +153,10 @@ constructor( private fun listenForGoneToAodOrDozing() { scope.launch { - powerInteractor.isAsleep - .sample( - combine( - startedKeyguardTransitionStep, - keyguardInteractor.isAodAvailable, - ::Pair - ), - ::toTriple - ) - .collect { (isAsleep, lastStartedStep, isAodAvailable) -> - if (lastStartedStep.to == KeyguardState.GONE && isAsleep) { - startTransitionTo( - toState = - if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING, - modeOnCanceled = TransitionModeOnCanceled.RESET, - ) - } - } + listenForSleepTransition( + from = KeyguardState.GONE, + modeOnCanceledFromStartedStep = { TransitionModeOnCanceled.RESET }, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index cb1571e7d702..bcad3324b258 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -33,7 +33,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.Utils.Companion.toQuad -import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject @@ -44,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -62,21 +62,24 @@ constructor( private val keyguardInteractor: KeyguardInteractor, private val flags: FeatureFlags, private val shadeRepository: ShadeRepository, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, private val glanceableHubTransitions: GlanceableHubTransitions, private val swipeToDismissInteractor: SwipeToDismissInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.LOCKSCREEN, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { listenForLockscreenToGone() listenForLockscreenToGoneDragging() - listenForLockscreenToOccluded() + listenForLockscreenToOccludedOrDreaming() listenForLockscreenToAodOrDozing() listenForLockscreenToPrimaryBouncer() listenForLockscreenToDreaming() @@ -115,6 +118,10 @@ constructor( } private fun listenForLockscreenToDreaming() { + if (KeyguardWmStateRefactor.isEnabled) { + return + } + val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING) scope.launch { keyguardInteractor.isAbleToDream @@ -157,7 +164,10 @@ constructor( if ( isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN ) { - startTransitionTo(KeyguardState.PRIMARY_BOUNCER) + startTransitionTo( + KeyguardState.PRIMARY_BOUNCER, + ownerReason = "#listenForLockscreenToPrimaryBouncer" + ) } } } @@ -254,7 +264,8 @@ constructor( transitionId = startTransitionTo( toState = KeyguardState.PRIMARY_BOUNCER, - animator = null, // transition will be manually controlled + animator = null, // transition will be manually controlled, + ownerReason = "#listenForLockscreenToPrimaryBouncerDragging" ) } } @@ -309,47 +320,51 @@ constructor( } } - private fun listenForLockscreenToOccluded() { - scope.launch { - keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect { - (isOccluded, keyguardState) -> - if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) { - startTransitionTo(KeyguardState.OCCLUDED) - } + private fun listenForLockscreenToOccludedOrDreaming() { + if (KeyguardWmStateRefactor.isEnabled) { + scope.launch { + keyguardOcclusionInteractor.showWhenLockedActivityInfo + .filter { it.isOnTop } + .sample(startedKeyguardState, ::Pair) + .collect { (taskInfo, startedState) -> + if (startedState == KeyguardState.LOCKSCREEN) { + startTransitionTo( + if (taskInfo.isDream()) { + KeyguardState.DREAMING + } else { + KeyguardState.OCCLUDED + } + ) + } + } + } + } else { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample(startedKeyguardState, ::Pair) + .collect { (isOccluded, keyguardState) -> + if (isOccluded && keyguardState == KeyguardState.LOCKSCREEN) { + startTransitionTo(KeyguardState.OCCLUDED) + } + } } } } private fun listenForLockscreenToAodOrDozing() { scope.launch { - powerInteractor.isAsleep - .sample( - combine( - startedKeyguardTransitionStep, - keyguardInteractor.isAodAvailable, - ::Pair - ), - ::toTriple - ) - .collect { (isAsleep, lastStartedStep, isAodAvailable) -> - if (lastStartedStep.to == KeyguardState.LOCKSCREEN && isAsleep) { - val toState = - if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING - val modeOnCanceled = - if ( - toState == KeyguardState.AOD && - lastStartedStep.from == KeyguardState.AOD - ) { - TransitionModeOnCanceled.REVERSE - } else { - TransitionModeOnCanceled.LAST_VALUE - } - startTransitionTo( - toState = toState, - modeOnCanceled = modeOnCanceled, - ) + listenForSleepTransition( + from = KeyguardState.LOCKSCREEN, + modeOnCanceledFromStartedStep = { startedStep -> + if ( + startedStep.to == KeyguardState.AOD && startedStep.from == KeyguardState.AOD + ) { + TransitionModeOnCanceled.REVERSE + } else { + TransitionModeOnCanceled.LAST_VALUE } } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index efb604d5dadc..f10327e02240 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -22,17 +22,17 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample -import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @SysUISingleton @@ -45,20 +45,23 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.OCCLUDED, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { listenForOccludedToLockscreenOrHub() listenForOccludedToDreaming() - listenForOccludedToAodOrDozing() + listenForOccludedToAsleep() listenForOccludedToGone() listenForOccludedToAlternateBouncer() listenForOccludedToPrimaryBouncer() @@ -90,43 +93,72 @@ constructor( } private fun listenForOccludedToLockscreenOrHub() { - scope.launch { - keyguardInteractor.isKeyguardOccluded - .sample( - keyguardInteractor.isKeyguardShowing, - startedKeyguardTransitionStep, - communalInteractor.isIdleOnCommunal, - ) - .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal) -> - // Occlusion signals come from the framework, and should interrupt any - // existing transition - if ( - !isOccluded && - isShowing && - lastStartedKeyguardState.to == KeyguardState.OCCLUDED - ) { - val to = - if (isIdleOnCommunal) { - KeyguardState.GLANCEABLE_HUB - } else { - KeyguardState.LOCKSCREEN - } - startTransitionTo(to) + if (KeyguardWmStateRefactor.isEnabled) { + scope.launch { + keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop + .filter { onTop -> !onTop } + .sample( + startedKeyguardState, + communalInteractor.isIdleOnCommunal, + ) + .collect { (_, startedState, isIdleOnCommunal) -> + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if (startedState == KeyguardState.OCCLUDED) { + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to) + } } - } + } + } else { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample( + keyguardInteractor.isKeyguardShowing, + startedKeyguardTransitionStep, + communalInteractor.isIdleOnCommunal, + ) + .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal) + -> + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if ( + !isOccluded && + isShowing && + lastStartedKeyguardState.to == KeyguardState.OCCLUDED + ) { + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to) + } + } + } } } private fun listenForOccludedToGone() { + if (KeyguardWmStateRefactor.isEnabled) { + // We don't think OCCLUDED to GONE is possible. You should always have to go via a + // *_BOUNCER state to end up GONE. Launching an activity over a dismissable keyguard + // dismisses it, and even "extend unlock" doesn't unlock the device in the background. + // If we're wrong - sorry, add it back here. + return + } + scope.launch { keyguardInteractor.isKeyguardOccluded .sample( - combine( - keyguardInteractor.isKeyguardShowing, - startedKeyguardTransitionStep, - ::Pair - ), - ::toTriple + keyguardInteractor.isKeyguardShowing, + startedKeyguardTransitionStep, ) .collect { (isOccluded, isShowing, lastStartedKeyguardState) -> // Occlusion signals come from the framework, and should interrupt any @@ -142,25 +174,12 @@ constructor( } } - private fun listenForOccludedToAodOrDozing() { - scope.launch { - powerInteractor.isAsleep - .sample( - combine( - startedKeyguardTransitionStep, - keyguardInteractor.isAodAvailable, - ::Pair - ), - ::toTriple - ) - .collect { (isAsleep, lastStartedStep, isAodAvailable) -> - if (lastStartedStep.to == KeyguardState.OCCLUDED && isAsleep) { - startTransitionTo( - if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING - ) - } - } - } + fun dismissToGone() { + scope.launch { startTransitionTo(KeyguardState.GONE) } + } + + private fun listenForOccludedToAsleep() { + scope.launch { listenForSleepTransition(from = KeyguardState.OCCLUDED) } } private fun listenForOccludedToAlternateBouncer() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index c5a28463bf7e..391dccc7f444 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -31,7 +31,6 @@ import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample -import com.android.systemui.util.kotlin.Utils.Companion.toQuad import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import com.android.wm.shell.animation.Interpolators @@ -42,6 +41,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch @@ -59,18 +59,21 @@ constructor( private val flags: FeatureFlags, private val keyguardSecurityModel: KeyguardSecurityModel, private val selectedUserInteractor: SelectedUserInteractor, - private val powerInteractor: PowerInteractor, + powerInteractor: PowerInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : TransitionInteractor( fromState = KeyguardState.PRIMARY_BOUNCER, transitionInteractor = transitionInteractor, mainDispatcher = mainDispatcher, bgDispatcher = bgDispatcher, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) { override fun start() { listenForPrimaryBouncerToGone() - listenForPrimaryBouncerToAodOrDozing() + listenForPrimaryBouncerToAsleep() listenForPrimaryBouncerToLockscreenHubOrOccluded() listenForPrimaryBouncerToDreamingLockscreenHosted() listenForTransitionToCamera(scope, keyguardInteractor) @@ -128,74 +131,88 @@ constructor( } private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() { - scope.launch { - keyguardInteractor.primaryBouncerShowing - .sample( - powerInteractor.isAwake, - startedKeyguardTransitionStep, - keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isDreaming, - keyguardInteractor.isActiveDreamLockscreenHosted, - communalInteractor.isIdleOnCommunal, - ) - .collect { - ( - isBouncerShowing, - isAwake, - lastStartedTransitionStep, - occluded, - isDreaming, - isActiveDreamLockscreenHosted, - isIdleOnCommunal) -> - if ( - !isBouncerShowing && - lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER && - isAwake && - !isActiveDreamLockscreenHosted - ) { - val toState = - if (occluded && !isDreaming) { - KeyguardState.OCCLUDED - } else if (isIdleOnCommunal) { - KeyguardState.GLANCEABLE_HUB - } else if (isDreaming) { - KeyguardState.DREAMING - } else { - KeyguardState.LOCKSCREEN - } - startTransitionTo(toState) + if (KeyguardWmStateRefactor.isEnabled) { + scope.launch { + keyguardInteractor.primaryBouncerShowing + .sample( + startedKeyguardTransitionStep, + powerInteractor.isAwake, + keyguardInteractor.isActiveDreamLockscreenHosted, + communalInteractor.isIdleOnCommunal + ) + .filter { (_, startedStep, _, _) -> + startedStep.to == KeyguardState.PRIMARY_BOUNCER } - } - } - } - - private fun listenForPrimaryBouncerToAodOrDozing() { - scope.launch { - keyguardInteractor.primaryBouncerShowing - .sample( - combine( - powerInteractor.isAsleep, + .collect { + ( + isBouncerShowing, + _, + isAwake, + isActiveDreamLockscreenHosted, + isIdleOnCommunal) -> + if ( + !maybeStartTransitionToOccludedOrInsecureCamera() && + !isBouncerShowing && + isAwake && + !isActiveDreamLockscreenHosted + ) { + val toState = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(toState) + } + } + } + } else { + scope.launch { + keyguardInteractor.primaryBouncerShowing + .sample( + powerInteractor.isAwake, startedKeyguardTransitionStep, - keyguardInteractor.isAodAvailable, - ::Triple - ), - ::toQuad - ) - .collect { (isBouncerShowing, isAsleep, lastStartedTransitionStep, isAodAvailable) - -> - if ( - !isBouncerShowing && - lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER && - isAsleep - ) { - startTransitionTo( - if (isAodAvailable) KeyguardState.AOD else KeyguardState.DOZING - ) + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.isDreaming, + keyguardInteractor.isActiveDreamLockscreenHosted, + communalInteractor.isIdleOnCommunal, + ) + .collect { + ( + isBouncerShowing, + isAwake, + lastStartedTransitionStep, + occluded, + isDreaming, + isActiveDreamLockscreenHosted, + isIdleOnCommunal) -> + if ( + !isBouncerShowing && + lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER && + isAwake && + !isActiveDreamLockscreenHosted + ) { + val toState = + if (occluded && !isDreaming) { + KeyguardState.OCCLUDED + } else if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else if (isDreaming) { + KeyguardState.DREAMING + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(toState) + } } - } + } } } + private fun listenForPrimaryBouncerToAsleep() { + scope.launch { listenForSleepTransition(from = KeyguardState.PRIMARY_BOUNCER) } + } + private fun listenForPrimaryBouncerToDreamingLockscreenHosted() { scope.launch { keyguardInteractor.primaryBouncerShowing diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 5410b10a4b93..f321bd7e13a0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -163,15 +163,18 @@ constructor( .distinctUntilChanged() /** Whether the keyguard is showing or not. */ + @Deprecated("Use KeyguardTransitionInteractor + KeyguardState") val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing /** Whether the keyguard is dismissible or not. */ val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible /** Whether the keyguard is occluded (covered by an activity). */ + @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED") val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded /** Whether the keyguard is going away. */ + @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.GONE") val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway /** Keyguard can be clipped at the top as the shade is dragged */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt new file mode 100644 index 000000000000..9aa2202b4100 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.app.ActivityManager.RunningTaskInfo +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardOcclusionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn + +/** + * Logic related to keyguard occlusion. The keyguard is occluded when an activity with + * FLAG_SHOW_WHEN_LOCKED is on top of the activity task stack, with that activity displaying on top + * of ("occluding") the lockscreen UI. Common examples of this are Google Maps Navigation and the + * secure camera. + * + * This should usually be used only by keyguard internal classes. Most System UI use cases should + * use [KeyguardTransitionInteractor] to see if we're in [KeyguardState.OCCLUDED] instead. + */ +@SysUISingleton +class KeyguardOcclusionInteractor +@Inject +constructor( + @Application scope: CoroutineScope, + val repository: KeyguardOcclusionRepository, + val powerInteractor: PowerInteractor, + val transitionInteractor: KeyguardTransitionInteractor, + val keyguardInteractor: KeyguardInteractor, +) { + val showWhenLockedActivityInfo = repository.showWhenLockedActivityInfo.asStateFlow() + + /** + * Whether a SHOW_WHEN_LOCKED activity is on top of the task stack. This does not necessarily + * mean we're OCCLUDED, as we could be GONE (unlocked), with an activity that can (but is not + * currently) displaying over the lockscreen. + * + * Transition interactors use this to determine when we should transition to the OCCLUDED state. + * + * Outside of the transition/occlusion interactors, you almost certainly don't want to use this. + * Instead, use KeyguardTransitionInteractor to figure out if we're in KeyguardState.OCCLUDED. + */ + val isShowWhenLockedActivityOnTop = showWhenLockedActivityInfo.map { it.isOnTop } + + /** Whether we should start a transition due to the power button launch gesture. */ + fun shouldTransitionFromPowerButtonGesture(): Boolean { + // powerButtonLaunchGestureTriggered remains true while we're awake from a power button + // gesture. Check that we were asleep or transitioning to asleep before starting a + // transition, to ensure we don't transition while moving between, for example, + // *_BOUNCER -> LOCKSCREEN. + return powerInteractor.detailedWakefulness.value.powerButtonLaunchGestureTriggered && + KeyguardState.deviceIsAsleepInState(transitionInteractor.getStartedState()) + } + + /** + * Whether the SHOW_WHEN_LOCKED activity was launched from the double tap power button gesture. + * This remains true while the activity is running and emits false once it is killed. + */ + val showWhenLockedActivityLaunchedFromPowerGesture = + merge( + // Emit true when the power launch gesture is triggered, since this means a + // SHOW_WHEN_LOCKED activity will be launched from the gesture (unless we're + // currently + // GONE, in which case we're going back to GONE and launching the insecure camera). + powerInteractor.detailedWakefulness + .sample(transitionInteractor.currentKeyguardState, ::Pair) + .map { (wakefulness, currentKeyguardState) -> + wakefulness.powerButtonLaunchGestureTriggered && + currentKeyguardState != KeyguardState.GONE + }, + // Emit false once that activity goes away. + isShowWhenLockedActivityOnTop.filter { !it }.map { false } + ) + .stateIn(scope, SharingStarted.Eagerly, false) + + /** + * Whether launching an occluding activity will automatically dismiss keyguard. This happens if + * the keyguard is dismissable. + */ + val occludingActivityWillDismissKeyguard = + keyguardInteractor.isKeyguardDismissible.stateIn(scope, SharingStarted.Eagerly, false) + + /** + * Called to let System UI know that WM says a SHOW_WHEN_LOCKED activity is on top (or no longer + * on top). + * + * This signal arrives from WM when a SHOW_WHEN_LOCKED activity is started or killed - it is + * never set directly by System UI. While we might be the reason the activity was started + * (launching the camera from the power button gesture), we ultimately only receive this signal + * once that activity starts. It's up to us to start the appropriate keyguard transitions, + * because that activity is going to be visible (or not) regardless. + */ + fun setWmNotifiedShowWhenLockedActivityOnTop( + showWhenLockedActivityOnTop: Boolean, + taskInfo: RunningTaskInfo? = null + ) { + repository.setShowWhenLockedActivityInfo(showWhenLockedActivityOnTop, taskInfo) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index b0a38811cdfc..8eb1a50086c6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -46,6 +46,7 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController @@ -55,6 +56,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -66,6 +68,7 @@ class KeyguardQuickAffordanceInteractor @Inject constructor( private val keyguardInteractor: KeyguardInteractor, + private val shadeInteractor: ShadeInteractor, private val lockPatternUtils: LockPatternUtils, private val keyguardStateController: KeyguardStateController, private val userTracker: UserTracker, @@ -100,9 +103,10 @@ constructor( quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, + shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(), biometricSettingsRepository.isCurrentUserInLockdown, - ) { affordance, isDozing, isKeyguardShowing, isUserInLockdown -> - if (!isDozing && isKeyguardShowing && !isUserInLockdown) { + ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown -> + if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) { affordance } else { KeyguardQuickAffordanceModel.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index d81f1f14158c..c28e49db9d37 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -41,6 +41,7 @@ constructor( private val powerInteractor: PowerInteractor, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, private val shadeInteractor: ShadeInteractor, + private val keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) { fun start() { @@ -91,6 +92,12 @@ constructor( } scope.launch { + keyguardInteractor.isKeyguardDismissible.collect { + logger.log(TAG, VERBOSE, "isKeyguardDismissable", it) + } + } + + scope.launch { keyguardInteractor.isAbleToDream.collect { logger.log(TAG, VERBOSE, "isAbleToDream", it) } @@ -125,5 +132,11 @@ constructor( logger.log(TAG, VERBOSE, "onCameraLaunchDetected", it) } } + + scope.launch { + keyguardOcclusionInteractor.showWhenLockedActivityInfo.collect { + logger.log(TAG, VERBOSE, "showWhenLockedActivityInfo", it) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 37b331cd8455..00902b419a97 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -20,6 +20,7 @@ package com.android.systemui.keyguard.domain.interactor import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER @@ -42,6 +43,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -56,11 +58,15 @@ class KeyguardTransitionInteractor @Inject constructor( @Application val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, private val repository: KeyguardTransitionRepository, private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>, private val fromPrimaryBouncerTransitionInteractor: dagger.Lazy<FromPrimaryBouncerTransitionInteractor>, private val fromAodTransitionInteractor: dagger.Lazy<FromAodTransitionInteractor>, + private val fromAlternateBouncerTransitionInteractor: + dagger.Lazy<FromAlternateBouncerTransitionInteractor>, + private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>, ) { private val TAG = this::class.simpleName @@ -207,6 +213,12 @@ constructor( .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) + /** Which keyguard state to use when the device goes to sleep. */ + val asleepKeyguardState: StateFlow<KeyguardState> = + keyguardRepository.isAodAvailable + .map { aodAvailable -> if (aodAvailable) AOD else DOZING } + .stateIn(scope, SharingStarted.Eagerly, DOZING) + /** * A pair of the most recent STARTED step, and the transition step immediately preceding it. The * transition framework enforces that the previous step is either a CANCELED or FINISHED step, @@ -368,7 +380,10 @@ constructor( when (val startedState = startedKeyguardState.replayCache.last()) { LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard() PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer() + ALTERNATE_BOUNCER -> + fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer() AOD -> fromAodTransitionInteractor.get().dismissAod() + DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing() else -> Log.e( "KeyguardTransitionInteractor", @@ -421,12 +436,17 @@ constructor( fromStatePredicate: (KeyguardState) -> Boolean, toStatePredicate: (KeyguardState) -> Boolean, ): Flow<Boolean> { + return isInTransitionWhere { from, to -> fromStatePredicate(from) && toStatePredicate(to) } + } + + fun isInTransitionWhere( + fromToStatePredicate: (KeyguardState, KeyguardState) -> Boolean + ): Flow<Boolean> { return repository.transitions .filter { it.transitionState != TransitionState.CANCELED } .mapLatest { it.transitionState != TransitionState.FINISHED && - fromStatePredicate(it.from) && - toStatePredicate(it.to) + fromToStatePredicate(it.from, it.to) } .distinctUntilChanged() } @@ -447,4 +467,16 @@ constructor( */ fun isFinishedInStateWhereValue(stateMatcher: (KeyguardState) -> Boolean) = stateMatcher(finishedKeyguardState.replayCache.last()) + + fun getCurrentState(): KeyguardState { + return currentKeyguardState.replayCache.last() + } + + fun getStartedState(): KeyguardState { + return startedKeyguardState.replayCache.last() + } + + fun getFinishedState(): KeyguardState { + return finishedKeyguardState.replayCache.last() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 4d731eccd9bb..8905c9e752de 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -43,7 +43,6 @@ constructor( private val scrimLogger: ScrimLogger, private val powerInteractor: PowerInteractor, ) { - init { listenForStartedKeyguardTransitionStep() } @@ -52,9 +51,7 @@ constructor( scope.launch { transitionInteractor.startedKeyguardTransitionStep.collect { scrimLogger.d(TAG, "listenForStartedKeyguardTransitionStep", it) - lightRevealScrimRepository.startRevealAmountAnimator( - willBeRevealedInState(it.to), - ) + lightRevealScrimRepository.startRevealAmountAnimator(willBeRevealedInState(it.to)) } } } @@ -89,25 +86,25 @@ constructor( companion object { - /** - * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given - * state after the transition is complete. If false, scrim will be fully hidden. - */ - private fun willBeRevealedInState(state: KeyguardState): Boolean { - return when (state) { - KeyguardState.OFF -> false - KeyguardState.DOZING -> false - KeyguardState.AOD -> false - KeyguardState.DREAMING -> true - KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true - KeyguardState.GLANCEABLE_HUB -> true - KeyguardState.ALTERNATE_BOUNCER -> true - KeyguardState.PRIMARY_BOUNCER -> true - KeyguardState.LOCKSCREEN -> true - KeyguardState.GONE -> true - KeyguardState.OCCLUDED -> true + /** + * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given + * state after the transition is complete. If false, scrim will be fully hidden. + */ + private fun willBeRevealedInState(state: KeyguardState): Boolean { + return when (state) { + KeyguardState.OFF -> false + KeyguardState.DOZING -> false + KeyguardState.AOD -> false + KeyguardState.DREAMING -> true + KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> true + KeyguardState.GLANCEABLE_HUB -> true + KeyguardState.ALTERNATE_BOUNCER -> true + KeyguardState.PRIMARY_BOUNCER -> true + KeyguardState.LOCKSCREEN -> true + KeyguardState.GONE -> true + KeyguardState.OCCLUDED -> true + } } - } val TAG = LightRevealScrimInteractor::class.simpleName!! } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index 3ccbdba6d58e..375df3e8f5f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -18,15 +18,20 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import android.util.Log +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.util.kotlin.sample import java.util.UUID import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -46,6 +51,8 @@ sealed class TransitionInteractor( val transitionInteractor: KeyguardTransitionInteractor, val mainDispatcher: CoroutineDispatcher, val bgDispatcher: CoroutineDispatcher, + val powerInteractor: PowerInteractor, + val keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) { val name = this::class.simpleName ?: "UnknownTransitionInteractor" abstract val transitionRepository: KeyguardTransitionRepository @@ -65,7 +72,11 @@ sealed class TransitionInteractor( suspend fun startTransitionTo( toState: KeyguardState, animator: ValueAnimator? = getDefaultAnimatorForTransitionsToState(toState), - modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE + modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE, + // Even more information about why the owner started this transition, if this is a dangerous + // transition (*cough* occlusion) where you'd be sad to not have all the info you can get in + // a bugreport. + ownerReason: String = "", ): UUID? { if ( fromState != transitionInteractor.startedKeyguardState.replayCache.last() && @@ -85,7 +96,7 @@ sealed class TransitionInteractor( return withContext(mainDispatcher) { transitionRepository.startTransition( TransitionInfo( - name, + name + if (ownerReason.isNotBlank()) "($ownerReason)" else "", fromState, toState, animator, @@ -95,24 +106,107 @@ sealed class TransitionInteractor( } } + /** + * Check whether we need to transition to [KeyguardState.OCCLUDED], based on the presence of a + * SHOW_WHEN_LOCKED activity, or back to [KeyguardState.GONE], for some power button launch + * gesture cases. If so, start the transition. + * + * Returns true if a transition was started, false otherwise. + */ + suspend fun maybeStartTransitionToOccludedOrInsecureCamera(): Boolean { + if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) { + if (transitionInteractor.getCurrentState() == KeyguardState.GONE) { + // If the current state is GONE when the launch gesture is triggered, it means we + // were in transition from GONE -> DOZING/AOD due to the first power button tap. The + // second tap indicates that the user's intent was actually to launch the unlocked + // (insecure) camera, so we should transition back to GONE. + startTransitionTo( + KeyguardState.GONE, + ownerReason = "Power button gesture while GONE" + ) + } else if (keyguardOcclusionInteractor.occludingActivityWillDismissKeyguard.value) { + // The double tap gesture occurred while not GONE (AOD/LOCKSCREEN/etc.), but the + // keyguard is dismissable. The activity launch will dismiss the keyguard, so we + // should transition to GONE. + startTransitionTo( + KeyguardState.GONE, + ownerReason = "Power button gesture on dismissable keyguard" + ) + } else { + // Otherwise, the double tap gesture occurred while not GONE and not dismissable, + // which means we will launch the secure camera, which OCCLUDES the keyguard. + startTransitionTo( + KeyguardState.OCCLUDED, + ownerReason = "Power button gesture on lockscreen" + ) + } + + return true + } else if (keyguardOcclusionInteractor.showWhenLockedActivityInfo.value.isOnTop) { + // A SHOW_WHEN_LOCKED activity is on top of the task stack. Transition to OCCLUDED so + // it's visible. + // TODO(b/307976454) - Centralize transition to DREAMING here. + startTransitionTo( + KeyguardState.OCCLUDED, + ownerReason = "SHOW_WHEN_LOCKED activity on top" + ) + + return true + } else { + // No transition needed, let the interactor figure out where to go. + return false + } + } + + /** + * Transition to the appropriate state when the device goes to sleep while in [from]. + * + * We could also just use [fromState], but it's more readable in the From*TransitionInteractor + * if you're explicitly declaring which state you're listening from. If you passed in the wrong + * state, [startTransitionTo] would complain anyway. + */ + suspend fun listenForSleepTransition( + from: KeyguardState, + modeOnCanceledFromStartedStep: (TransitionStep) -> TransitionModeOnCanceled = { + TransitionModeOnCanceled.LAST_VALUE + } + ) { + powerInteractor.isAsleep + .filter { isAsleep -> isAsleep } + .sample(startedKeyguardTransitionStep) + .filter { startedStep -> startedStep.to == from } + .map(modeOnCanceledFromStartedStep) + .collect { modeOnCanceled -> + startTransitionTo( + toState = transitionInteractor.asleepKeyguardState.value, + modeOnCanceled = modeOnCanceled, + ownerReason = "Sleep transition triggered" + ) + } + } + /** This signal may come in before the occlusion signal, and can provide a custom transition */ fun listenForTransitionToCamera( scope: CoroutineScope, keyguardInteractor: KeyguardInteractor, ) { - scope.launch { - keyguardInteractor.onCameraLaunchDetected - .sample(transitionInteractor.finishedKeyguardState) - .collect { finishedKeyguardState -> - // Other keyguard state transitions may trigger on the first power button push, - // so use the last finishedKeyguardState to determine the overriding FROM state - if (finishedKeyguardState == fromState) { - startTransitionTo( - toState = KeyguardState.OCCLUDED, - modeOnCanceled = TransitionModeOnCanceled.RESET, - ) + if (!KeyguardWmStateRefactor.isEnabled) { + scope.launch { + keyguardInteractor.onCameraLaunchDetected + .sample(transitionInteractor.finishedKeyguardState) + .collect { finishedKeyguardState -> + // Other keyguard state transitions may trigger on the first power button + // push, + // so use the last finishedKeyguardState to determine the overriding FROM + // state + if (finishedKeyguardState == fromState) { + startTransitionTo( + toState = KeyguardState.OCCLUDED, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) + } } - } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt index a9eec18319c3..7e39a884a69e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToGoneTransitionViewModel.kt @@ -43,6 +43,10 @@ constructor( to = KeyguardState.GONE, ) + /** + * AOD -> GONE should fade out the lockscreen contents. This transition plays both during wake + * and unlock, and also during insecure camera launch (which is GONE -> AOD (canceled) -> GONE). + */ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { var startAlpha = 1f return transitionAnimation.sharedFlow( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt index 105a7ed52311..445575f7e55d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt @@ -16,12 +16,15 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.MathUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow /** Breaks down AOD->OCCLUDED transition into discrete steps for corresponding views to consume. */ @SysUISingleton @@ -37,5 +40,23 @@ constructor( to = KeyguardState.OCCLUDED, ) + /** + * Fade out the lockscreen during a transition to OCCLUDED. + * + * This happens when pressing the power button while a SHOW_WHEN_LOCKED activity is on the top + * of the task stack, as well as when the power button is double tapped on the LOCKSCREEN (the + * first tap transitions to AOD, the second cancels that transition and starts AOD -> OCCLUDED. + */ + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var currentAlpha = 0f + return transitionAnimation.sharedFlow( + duration = 250.milliseconds, + startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements. + onStart = { currentAlpha = viewState.alpha() }, + onStep = { MathUtils.lerp(currentAlpha, 0f, it) }, + onCancel = { 0f }, + ) + } + override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt new file mode 100644 index 000000000000..c0b11959cbd9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import android.util.MathUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow + +/** + * Breaks down DOZING->OCCLUDED transition into discrete steps for corresponding views to consume. + */ +@SysUISingleton +class DozingToOccludedTransitionViewModel +@Inject +constructor( + animationFlow: KeyguardTransitionAnimationFlow, +) : DeviceEntryIconTransition { + private val transitionAnimation = + animationFlow.setup( + duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION, + from = KeyguardState.DOZING, + to = KeyguardState.OCCLUDED, + ) + + /** + * Fade out the lockscreen during a transition to OCCLUDED. + * + * This happens when pressing the power button while a SHOW_WHEN_LOCKED activity is on the top + * of the task stack, as well as when the power button is double tapped on the LOCKSCREEN (the + * first tap transitions to DOZING, the second cancels that transition and starts DOZING -> + * OCCLUDED. + */ + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var currentAlpha = 0f + return transitionAnimation.sharedFlow( + duration = 250.milliseconds, + startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements. + onStart = { currentAlpha = viewState.alpha() }, + onStep = { MathUtils.lerp(currentAlpha, 0f, it) }, + onCancel = { 0f }, + ) + } + + override val deviceEntryParentViewAlpha = transitionAnimation.immediatelyTransitionTo(0f) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 1760b927a6cd..f848717c2170 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -73,8 +73,10 @@ constructor( AlternateBouncerToGoneTransitionViewModel, private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, + private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val dozingToGoneTransitionViewModel: DozingToGoneTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, + private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, private val glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, @@ -170,8 +172,10 @@ constructor( alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, aodToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState), + aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToGoneTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, + dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dreamingToLockscreenTransitionViewModel.lockscreenAlpha, glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, goneToAodTransitionViewModel.enterFromTopAnimationAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt deleted file mode 100644 index fc452288f86d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialog.kt +++ /dev/null @@ -1,33 +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.mediaprojection.devicepolicy - -import android.content.Context -import com.android.systemui.res.R -import com.android.systemui.statusbar.phone.SystemUIDialog - -/** Dialog that shows that screen capture is disabled on this device. */ -class ScreenCaptureDisabledDialog(context: Context) : SystemUIDialog(context) { - - init { - setTitle(context.getString(R.string.screen_capturing_disabled_by_policy_dialog_title)) - setMessage( - context.getString(R.string.screen_capturing_disabled_by_policy_dialog_description) - ) - setIcon(R.drawable.ic_cast) - setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> cancel() } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialogDelegate.kt new file mode 100644 index 000000000000..8aed535956b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDisabledDialogDelegate.kt @@ -0,0 +1,44 @@ +/* + * 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.mediaprojection.devicepolicy + +import android.content.DialogInterface.BUTTON_POSITIVE +import android.content.res.Resources +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject + +/** Dialog that shows that screen capture is disabled on this device. */ +class ScreenCaptureDisabledDialogDelegate @Inject constructor( + @Main private val resources: Resources, + private val systemUIDialogFactory: SystemUIDialog.Factory +) : SystemUIDialog.Delegate { + + override fun createDialog(): SystemUIDialog { + val dialog = systemUIDialogFactory.create(this) + dialog.setTitle(resources.getString(R.string.screen_capturing_disabled_by_policy_dialog_title)) + dialog.setMessage( + resources.getString(R.string.screen_capturing_disabled_by_policy_dialog_description) + ) + dialog.setIcon(R.drawable.ic_cast) + dialog.setButton(BUTTON_POSITIVE, resources.getString(android.R.string.ok)) { + _, _ -> dialog.cancel() + } + + return dialog + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index 8b034b293dcb..17f9cafcb650 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -59,7 +59,7 @@ import com.android.systemui.mediaprojection.MediaProjectionServiceHelper; import com.android.systemui.mediaprojection.SessionCreationSource; import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; -import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; +import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.AlertDialogWithDelegate; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -79,6 +79,7 @@ public class MediaProjectionPermissionActivity extends Activity private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver; private final StatusBarManager mStatusBarManager; private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; + private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate; private String mPackageName; private int mUid; @@ -93,14 +94,17 @@ public class MediaProjectionPermissionActivity extends Activity private boolean mUserSelectingTask = false; @Inject - public MediaProjectionPermissionActivity(FeatureFlags featureFlags, + public MediaProjectionPermissionActivity( + FeatureFlags featureFlags, Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver, StatusBarManager statusBarManager, - MediaProjectionMetricsLogger mediaProjectionMetricsLogger) { + MediaProjectionMetricsLogger mediaProjectionMetricsLogger, + ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate) { mFeatureFlags = featureFlags; mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver; mStatusBarManager = statusBarManager; mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger; + mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate; } @Override @@ -315,10 +319,7 @@ public class MediaProjectionPermissionActivity extends Activity final UserHandle hostUserHandle = getHostUserHandle(); if (mScreenCaptureDevicePolicyResolver.get() .isScreenCaptureCompletelyDisabled(hostUserHandle)) { - // Using application context for the dialog, instead of the activity context, so we get - // the correct screen width when in split screen. - Context dialogContext = getApplicationContext(); - AlertDialog dialog = new ScreenCaptureDisabledDialog(dialogContext); + AlertDialog dialog = mScreenCaptureDisabledDialogDelegate.createDialog(); setUpDialog(dialog); dialog.show(); return true; diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt index cc8cc5165e4f..d6629e07d87a 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModel.kt @@ -18,14 +18,18 @@ package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel import android.app.ActivityManager.RunningTaskInfo import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.withContext class TaskSwitcherNotificationViewModel @@ -36,21 +40,30 @@ constructor( ) { val uiState: Flow<TaskSwitcherNotificationUiState> = - interactor.taskSwitchChanges.map { taskSwitchChange -> - Log.d(TAG, "taskSwitchChange: $taskSwitchChange") - when (taskSwitchChange) { - is TaskSwitchState.TaskSwitched -> { - TaskSwitcherNotificationUiState.Showing( - projectedTask = taskSwitchChange.projectedTask, - foregroundTask = taskSwitchChange.foregroundTask, - ) + interactor.taskSwitchChanges + .map { taskSwitchChange -> + Log.d(TAG, "taskSwitchChange: $taskSwitchChange") + when (taskSwitchChange) { + is TaskSwitchState.TaskSwitched -> { + TaskSwitcherNotificationUiState.Showing( + projectedTask = taskSwitchChange.projectedTask, + foregroundTask = taskSwitchChange.foregroundTask, + ) + } + is TaskSwitchState.NotProjectingTask, + is TaskSwitchState.TaskUnchanged -> { + TaskSwitcherNotificationUiState.NotShowing + } } - is TaskSwitchState.NotProjectingTask, - is TaskSwitchState.TaskUnchanged -> { - TaskSwitcherNotificationUiState.NotShowing + } + .transformLatest { uiState -> + emit(uiState) + if (uiState is TaskSwitcherNotificationUiState.Showing) { + delay(NOTIFICATION_MAX_SHOW_DURATION) + Log.d(TAG, "Auto hiding notification after $NOTIFICATION_MAX_SHOW_DURATION") + emit(TaskSwitcherNotificationUiState.NotShowing) } } - } suspend fun onSwitchTaskClicked(task: RunningTaskInfo) { interactor.switchProjectedTask(task) @@ -60,6 +73,7 @@ constructor( withContext(backgroundDispatcher) { interactor.goBackToTask(task) } companion object { + @VisibleForTesting val NOTIFICATION_MAX_SHOW_DURATION = 5.seconds private const val TAG = "TaskSwitchNotifVM" } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java index 152f193be3f9..9f7d1b34151a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java @@ -19,6 +19,7 @@ package com.android.systemui.navigationbar; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; + import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG; import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification; @@ -28,7 +29,6 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.hardware.display.DisplayManager; import android.os.Bundle; -import android.os.Handler; import android.os.RemoteException; import android.os.Trace; import android.os.UserHandle; @@ -70,9 +70,11 @@ import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; import java.util.Optional; +import java.util.concurrent.Executor; import javax.inject.Inject; + @SysUISingleton public class NavigationBarControllerImpl implements ConfigurationController.ConfigurationListener, @@ -82,7 +84,7 @@ public class NavigationBarControllerImpl implements private static final String TAG = NavigationBarControllerImpl.class.getSimpleName(); private final Context mContext; - private final Handler mHandler; + private final Executor mExecutor; private final NavigationBarComponent.Factory mNavigationBarComponentFactory; private final SecureSettings mSecureSettings; private final DisplayTracker mDisplayTracker; @@ -119,7 +121,7 @@ public class NavigationBarControllerImpl implements NavigationModeController navigationModeController, SysUiState sysUiFlagsContainer, CommandQueue commandQueue, - @Main Handler mainHandler, + @Main Executor mainExecutor, ConfigurationController configurationController, NavBarHelper navBarHelper, TaskbarDelegate taskbarDelegate, @@ -133,7 +135,7 @@ public class NavigationBarControllerImpl implements SecureSettings secureSettings, DisplayTracker displayTracker) { mContext = context; - mHandler = mainHandler; + mExecutor = mainExecutor; mNavigationBarComponentFactory = navigationBarComponentFactory; mSecureSettings = secureSettings; mDisplayTracker = displayTracker; @@ -193,7 +195,7 @@ public class NavigationBarControllerImpl implements mNavMode = mode; updateAccessibilityButtonModeIfNeeded(); - mHandler.post(() -> { + mExecutor.execute(() -> { // create/destroy nav bar based on nav mode only in unfolded state if (oldMode != mNavMode) { updateNavbarForTaskbar(); diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt index e1d1ec207938..5432793d8117 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt @@ -20,20 +20,24 @@ data class WakefulnessModel( * the [KeyguardTransitionInteractor]. */ internal val internalWakefulnessState: WakefulnessState = WakefulnessState.AWAKE, - val lastWakeReason: WakeSleepReason = WakeSleepReason.OTHER, val lastSleepReason: WakeSleepReason = WakeSleepReason.OTHER, - /** + /** * Whether the power button double tap gesture was triggered since the last time went to sleep. * If this value is true while [isAsleep]=true, it means we'll be waking back up shortly. If it * is true while [isAwake]=true, it means we're awake because of the button gesture. * - * This value remains true until the next time [isAsleep]=true. + * This value remains true until the next time [isAsleep]=true, since it would otherwise be + * totally arbitrary at what point we decide the gesture was no longer "triggered". Since a + * sleep event is guaranteed to arrive prior to the next power button gesture (as the first tap + * of the double tap always begins a sleep transition), this will always be reset to false prior + * to a subsequent power gesture. */ val powerButtonLaunchGestureTriggered: Boolean = false, ) { - fun isAwake() = internalWakefulnessState == WakefulnessState.AWAKE || + fun isAwake() = + internalWakefulnessState == WakefulnessState.AWAKE || internalWakefulnessState == WakefulnessState.STARTING_TO_WAKE fun isAsleep() = !isAwake() @@ -48,11 +52,10 @@ data class WakefulnessModel( fun isAsleepFrom(wakeSleepReason: WakeSleepReason) = isAsleep() && lastSleepReason == wakeSleepReason - fun isAwakeOrAsleepFrom(reason: WakeSleepReason) = - isAsleepFrom(reason) || isAwakeFrom(reason) + fun isAwakeOrAsleepFrom(reason: WakeSleepReason) = isAsleepFrom(reason) || isAwakeFrom(reason) fun isAwakeFromTapOrGesture(): Boolean { - return isAwake() && (lastWakeReason == WakeSleepReason.TAP || - lastWakeReason == WakeSleepReason.GESTURE) + return isAwake() && + (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java index 0dd0a60b128a..52cf4ec57e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java @@ -481,7 +481,8 @@ public class InternetDialogDelegate implements mSecondaryMobileTitleText.setTextAppearance( R.style.TextAppearance_InternetDialog_Active); - TextView mSecondaryMobileSummaryText = mDialogView.requireViewById(R.id.secondary_mobile_summary); + TextView mSecondaryMobileSummaryText = + mDialogView.requireViewById(R.id.secondary_mobile_summary); summary = getMobileNetworkSummary(autoSwitchNonDdsSubId); if (!TextUtils.isEmpty(summary)) { mSecondaryMobileSummaryText.setText( diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index 80f11f1e1874..1c07d00e4195 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -40,7 +40,7 @@ import com.android.systemui.flags.Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPR import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.SessionCreationSource import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver -import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog +import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate import com.android.systemui.qs.tiles.RecordIssueTile import com.android.systemui.res.R import com.android.systemui.screenrecord.RecordingService @@ -65,6 +65,7 @@ constructor( private val devicePolicyResolver: dagger.Lazy<ScreenCaptureDevicePolicyResolver>, private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, private val userFileManager: UserFileManager, + private val screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate, @Assisted private val onStarted: Runnable, ) : SystemUIDialog.Delegate { @@ -124,7 +125,7 @@ constructor( .isScreenCaptureCompletelyDisabled(UserHandle.of(userTracker.userId)) ) { mainExecutor.execute { - ScreenCaptureDisabledDialog(context).show() + screenCaptureDisabledDialogDelegate.createDialog().show() screenRecordSwitch.isChecked = false } return@execute diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index a4ba2a241275..8fe84c98866b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -42,11 +42,9 @@ import com.android.systemui.flags.Flags; import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.mediaprojection.SessionCreationSource; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; -import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; +import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.CallbackController; import dagger.Lazy; @@ -71,18 +69,19 @@ public class RecordingController private CountDownTimer mCountDownTimer = null; private final Executor mMainExecutor; private final BroadcastDispatcher mBroadcastDispatcher; - private final Context mContext; private final FeatureFlags mFlags; - private final UserContextProvider mUserContextProvider; private final UserTracker mUserTracker; private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; - private final SystemUIDialog.Factory mDialogFactory; + private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate; + private final ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory; + private final ScreenRecordPermissionDialogDelegate.Factory + mScreenRecordPermissionDialogDelegateFactory; protected static final String INTENT_UPDATE_STATE = "com.android.systemui.screenrecord.UPDATE_STATE"; protected static final String EXTRA_STATE = "extra_state"; - private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners = + private final CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners = new CopyOnWriteArrayList<>(); private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver; @@ -115,24 +114,26 @@ public class RecordingController * Create a new RecordingController */ @Inject - public RecordingController(@Main Executor mainExecutor, + public RecordingController( + @Main Executor mainExecutor, BroadcastDispatcher broadcastDispatcher, - Context context, FeatureFlags flags, - UserContextProvider userContextProvider, Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver, UserTracker userTracker, MediaProjectionMetricsLogger mediaProjectionMetricsLogger, - SystemUIDialog.Factory dialogFactory) { + ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate, + ScreenRecordDialogDelegate.Factory screenRecordDialogFactory, + ScreenRecordPermissionDialogDelegate.Factory + screenRecordPermissionDialogDelegateFactory) { mMainExecutor = mainExecutor; - mContext = context; mFlags = flags; mDevicePolicyResolver = devicePolicyResolver; mBroadcastDispatcher = broadcastDispatcher; - mUserContextProvider = userContextProvider; mUserTracker = userTracker; mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger; - mDialogFactory = dialogFactory; + mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate; + mScreenRecordDialogFactory = screenRecordDialogFactory; + mScreenRecordPermissionDialogDelegateFactory = screenRecordPermissionDialogDelegateFactory; BroadcastOptions options = BroadcastOptions.makeBasic(); options.setInteractive(true); @@ -163,27 +164,18 @@ public class RecordingController if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES) && mDevicePolicyResolver.get() .isScreenCaptureCompletelyDisabled(getHostUserHandle())) { - return new ScreenCaptureDisabledDialog(mContext); + return mScreenCaptureDisabledDialogDelegate.createDialog(); } mMediaProjectionMetricsLogger.notifyProjectionInitiated( getHostUid(), SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER); - return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING) - ? mDialogFactory.create(new ScreenRecordPermissionDialogDelegate( - getHostUserHandle(), - getHostUid(), - /* controller= */ this, - activityStarter, - mUserContextProvider, - onStartRecordingClicked, - mMediaProjectionMetricsLogger, - mDialogFactory)) - : new ScreenRecordDialog( - context, - /* controller= */ this, - mUserContextProvider, - onStartRecordingClicked); + return (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING) + ? mScreenRecordPermissionDialogDelegateFactory + .create(this, getHostUserHandle(), getHostUid(), onStartRecordingClicked) + : mScreenRecordDialogFactory + .create(this, onStartRecordingClicked)) + .createDialog(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialogDelegate.java index b98093e50920..9f1447b1f509 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialogDelegate.java @@ -49,52 +49,69 @@ import com.android.systemui.res.R; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.SystemUIDialog; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; + import java.util.Arrays; import java.util.List; /** * Dialog to select screen recording options */ -public class ScreenRecordDialog extends SystemUIDialog { +public class ScreenRecordDialogDelegate implements SystemUIDialog.Delegate { private static final List<ScreenRecordingAudioSource> MODES = Arrays.asList(INTERNAL, MIC, MIC_AND_INTERNAL); private static final long DELAY_MS = 3000; private static final long INTERVAL_MS = 1000; - private final RecordingController mController; + private final SystemUIDialog.Factory mSystemUIDialogFactory; private final UserContextProvider mUserContextProvider; - @Nullable + private final RecordingController mController; private final Runnable mOnStartRecordingClicked; private Switch mTapsSwitch; private Switch mAudioSwitch; private Spinner mOptions; - public ScreenRecordDialog(Context context, - RecordingController controller, - UserContextProvider userContextProvider, - @Nullable Runnable onStartRecordingClicked) { - super(context); - mController = controller; + @AssistedFactory + public interface Factory { + ScreenRecordDialogDelegate create( + RecordingController recordingController, + @Nullable Runnable onStartRecordingClicked + ); + } + + @AssistedInject + public ScreenRecordDialogDelegate( + SystemUIDialog.Factory systemUIDialogFactory, + UserContextProvider userContextProvider, + @Assisted RecordingController controller, + @Assisted @Nullable Runnable onStartRecordingClicked) { + mSystemUIDialogFactory = systemUIDialogFactory; mUserContextProvider = userContextProvider; + mController = controller; mOnStartRecordingClicked = onStartRecordingClicked; } @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public SystemUIDialog createDialog() { + return mSystemUIDialogFactory.create(this); + } - Window window = getWindow(); + @Override + public void onCreate(SystemUIDialog dialog, Bundle savedInstanceState) { + Window window = dialog.getWindow(); window.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); window.setGravity(Gravity.CENTER); - setTitle(R.string.screenrecord_title); + dialog.setTitle(R.string.screenrecord_title); - setContentView(R.layout.screen_record_dialog); + dialog.setContentView(R.layout.screen_record_dialog); - TextView cancelBtn = findViewById(R.id.button_cancel); - cancelBtn.setOnClickListener(v -> dismiss()); - TextView startBtn = findViewById(R.id.button_start); + TextView cancelBtn = dialog.findViewById(R.id.button_cancel); + cancelBtn.setOnClickListener(v -> dialog.dismiss()); + TextView startBtn = dialog.findViewById(R.id.button_start); startBtn.setOnClickListener(v -> { if (mOnStartRecordingClicked != null) { // Note that it is important to run this callback before dismissing, so that the @@ -104,13 +121,13 @@ public class ScreenRecordDialog extends SystemUIDialog { // Start full-screen recording requestScreenCapture(/* captureTarget= */ null); - dismiss(); + dialog.dismiss(); }); - mAudioSwitch = findViewById(R.id.screenrecord_audio_switch); - mTapsSwitch = findViewById(R.id.screenrecord_taps_switch); - mOptions = findViewById(R.id.screen_recording_options); - ArrayAdapter a = new ScreenRecordingAdapter(getContext().getApplicationContext(), + mAudioSwitch = dialog.findViewById(R.id.screenrecord_audio_switch); + mTapsSwitch = dialog.findViewById(R.id.screenrecord_taps_switch); + mOptions = dialog.findViewById(R.id.screen_recording_options); + ArrayAdapter a = new ScreenRecordingAdapter(dialog.getContext().getApplicationContext(), android.R.layout.simple_spinner_dropdown_item, MODES); a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index 3eb26f498921..ba775cd3cd82 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -46,17 +46,20 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.SystemUIDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject /** Dialog to select screen recording options */ -class ScreenRecordPermissionDialogDelegate( - private val hostUserHandle: UserHandle, - private val hostUid: Int, - private val controller: RecordingController, +class ScreenRecordPermissionDialogDelegate @AssistedInject constructor( + @Assisted private val hostUserHandle: UserHandle, + @Assisted private val hostUid: Int, + @Assisted private val controller: RecordingController, private val activityStarter: ActivityStarter, private val userContextProvider: UserContextProvider, - private val onStartRecordingClicked: Runnable?, + @Assisted private val onStartRecordingClicked: Runnable?, mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, - private val systemUIDialogFactory: SystemUIDialog.Factory + private val systemUIDialogFactory: SystemUIDialog.Factory, ) : BaseMediaProjectionPermissionDialogDelegate<SystemUIDialog>( createOptionList(), @@ -65,8 +68,19 @@ class ScreenRecordPermissionDialogDelegate( mediaProjectionMetricsLogger, R.drawable.ic_screenrecord, R.color.screenrecord_icon_color - ), - SystemUIDialog.Delegate { + ), SystemUIDialog.Delegate { + + + @AssistedFactory + interface Factory { + fun create( + recordingController: RecordingController, + hostUserHandle: UserHandle, + hostUid: Int, + onStartRecordingClicked: Runnable? + ): ScreenRecordPermissionDialogDelegate + } + private lateinit var tapsSwitch: Switch private lateinit var tapsSwitchContainer: ViewGroup private lateinit var tapsView: View diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt new file mode 100644 index 000000000000..2294fc0be520 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.animation.Animator +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.Display +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.res.R + +/** + * Legacy implementation of screenshot view methods. Just proxies the calls down into the original + * ScreenshotView. + */ +class LegacyScreenshotViewProxy(context: Context) : ScreenshotViewProxy { + override val view: ScreenshotView = + LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView + override val internalInsetsListener: ViewTreeObserver.OnComputeInternalInsetsListener + override val screenshotPreview: View + + override var defaultDisplay: Int = Display.DEFAULT_DISPLAY + set(value) { + view.setDefaultDisplay(value) + } + override var defaultTimeoutMillis: Long = 6000 + set(value) { + view.setDefaultTimeoutMillis(value) + } + override var onKeyListener: View.OnKeyListener? = null + set(value) { + view.setOnKeyListener(value) + } + override var flags: FeatureFlags? = null + set(value) { + view.setFlags(value) + } + override var packageName: String = "" + set(value) { + view.setPackageName(value) + } + override var logger: UiEventLogger? = null + set(value) { + view.setUiEventLogger(value) + } + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + set(value) { + view.setCallbacks(value) + } + override var screenshot: ScreenshotData? = null + set(value) { + view.setScreenshot(value) + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override val isDismissing + get() = view.isDismissing + override val isPendingSharedTransition + get() = view.isPendingSharedTransition + + init { + internalInsetsListener = view + screenshotPreview = view.screenshotPreview + } + + override fun reset() = view.reset() + override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets) + override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets) + + override fun badgeScreenshot(userBadgedIcon: Drawable) = view.badgeScreenshot(userBadgedIcon) + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator = + view.createScreenshotDropInAnimation(screenRect, showFlash) + + override fun addQuickShareChip(quickShareAction: Notification.Action) = + view.addQuickShareChip(quickShareAction) + + override fun setChipIntents(imageData: ScreenshotController.SavedImageData) = + view.setChipIntents(imageData) + + override fun animateDismissal() = view.animateDismissal() + + override fun showScrollChip(packageName: String, onClick: Runnable) = + view.showScrollChip(packageName, onClick) + + override fun hideScrollChip() = view.hideScrollChip() + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean + ) = + view.prepareScrollingTransition( + response, + screenBitmap, + newScreenshot, + screenshotTakenInPortrait + ) + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) = view.startLongScreenshotTransition(transitionDestination, onTransitionEnd, longScreenshot) + + override fun restoreNonScrollingUi() = view.restoreNonScrollingUi() + + override fun stopInputListening() = view.stopInputListening() + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun addOnAttachStateChangeListener(listener: View.OnAttachStateChangeListener) = + view.addOnAttachStateChangeListener(listener) + + override fun findOnBackInvokedDispatcher(): OnBackInvokedDispatcher? = + view.findOnBackInvokedDispatcher() + + override fun getViewTreeObserver(): ViewTreeObserver = view.viewTreeObserver + + override fun post(runnable: Runnable) { + view.post(runnable) + } + + class Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context): ScreenshotViewProxy { + return LegacyScreenshotViewProxy(context) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index ee3e7ba9e8b3..13448d258a2c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -65,7 +65,6 @@ import android.view.Display; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.ScrollCaptureResponse; @@ -165,7 +164,7 @@ public class ScreenshotController { /** * Structure returned by the SaveImageInBackgroundTask */ - static class SavedImageData { + public static class SavedImageData { public Uri uri; public List<Notification.Action> smartActions; public Notification.Action quickShareAction; @@ -237,6 +236,7 @@ public class ScreenshotController { private final WindowContext mContext; private final FeatureFlags mFlags; + private final ScreenshotViewProxy mViewProxy; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -272,7 +272,6 @@ public class ScreenshotController { respondToKeyDismissal(); }; - private ScreenshotView mScreenshotView; private final MessageContainerController mMessageContainerController; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; @@ -305,6 +304,7 @@ public class ScreenshotController { ScreenshotController( Context context, FeatureFlags flags, + ScreenshotViewProxy.Factory viewProxyFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, ScrollCaptureClient scrollCaptureClient, @@ -360,6 +360,8 @@ public class ScreenshotController { mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; + mViewProxy = viewProxyFactory.getProxy(mContext); + mAccessibilityManager = AccessibilityManager.getInstance(mContext); // Setup the window that we are going to use @@ -461,7 +463,7 @@ public class ScreenshotController { // The window is focusable by default setWindowFocusable(true); - mScreenshotView.requestFocus(); + mViewProxy.requestFocus(); enqueueScrollCaptureRequest(screenshot.getUserHandle()); @@ -485,10 +487,10 @@ public class ScreenshotController { mMessageContainerController.onScreenshotTaken(screenshot); }); - mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( + mViewProxy.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( mContext.getDrawable(R.drawable.overlay_badge_background), screenshot.getUserHandle())); - mScreenshotView.setScreenshot(screenshot); + mViewProxy.setScreenshot(screenshot); // ignore system bar insets for the purpose of window layout mWindow.getDecorView().setOnApplyWindowInsetsListener( @@ -503,31 +505,31 @@ public class ScreenshotController { void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) { withWindowAttached(() -> { if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) { - mScreenshotView.announceForAccessibility(mContext.getResources().getString( + mViewProxy.announceForAccessibility(mContext.getResources().getString( R.string.screenshot_saving_work_profile_title)); } else { - mScreenshotView.announceForAccessibility( + mViewProxy.announceForAccessibility( mContext.getResources().getString(R.string.screenshot_saving_title)); } }); - mScreenshotView.reset(); + mViewProxy.reset(); - if (mScreenshotView.isAttachedToWindow()) { + if (mViewProxy.isAttachedToWindow()) { // if we didn't already dismiss for another reason - if (!mScreenshotView.isDismissing()) { + if (!mViewProxy.isDismissing()) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, oldPackageName); } if (DEBUG_WINDOW) { Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. " - + "(dismissing=" + mScreenshotView.isDismissing() + ")"); + + "(dismissing=" + mViewProxy.isDismissing() + ")"); } } - mScreenshotView.setPackageName(mPackageName); + mViewProxy.setPackageName(mPackageName); - mScreenshotView.updateOrientation( + mViewProxy.updateOrientation( mWindowManager.getCurrentWindowMetrics().getWindowInsets()); } @@ -539,7 +541,7 @@ public class ScreenshotController { Log.d(TAG, "dismissScreenshot"); } // If we're already animating out, don't restart the animation - if (mScreenshotView.isDismissing()) { + if (mViewProxy.isDismissing()) { if (DEBUG_DISMISS) { Log.v(TAG, "Already dismissing, ignoring duplicate command"); } @@ -547,11 +549,11 @@ public class ScreenshotController { } mUiEventLogger.log(event, 0, mPackageName); mScreenshotHandler.cancelTimeout(); - mScreenshotView.animateDismissal(); + mViewProxy.animateDismissal(); } boolean isPendingSharedTransition() { - return mScreenshotView.isPendingSharedTransition(); + return mViewProxy.isPendingSharedTransition(); } // Any cleanup needed when the service is being destroyed. @@ -576,7 +578,7 @@ public class ScreenshotController { private void releaseMediaPlayer() { if (mScreenshotSoundController == null) return; - mScreenshotSoundController.releaseScreenshotSound(); + mScreenshotSoundController.releaseScreenshotSoundAsync(); } private void respondToKeyDismissal() { @@ -591,18 +593,15 @@ public class ScreenshotController { Log.d(TAG, "reloadAssets()"); } - // Inflate the screenshot layout - mScreenshotView = (ScreenshotView) - LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); - mMessageContainerController.setView(mScreenshotView); - mScreenshotView.addOnAttachStateChangeListener( + mMessageContainerController.setView(mViewProxy.getView()); + mViewProxy.addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View v) { if (DEBUG_INPUT) { Log.d(TAG, "Registering Predictive Back callback"); } - mScreenshotView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback( + mViewProxy.findOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback); } @@ -611,11 +610,12 @@ public class ScreenshotController { if (DEBUG_INPUT) { Log.d(TAG, "Unregistering Predictive Back callback"); } - mScreenshotView.findOnBackInvokedDispatcher() + mViewProxy.findOnBackInvokedDispatcher() .unregisterOnBackInvokedCallback(mOnBackInvokedCallback); } }); - mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() { + mViewProxy.setLogger(mUiEventLogger); + mViewProxy.setCallbacks(new ScreenshotView.ScreenshotViewCallback() { @Override public void onUserInteraction() { if (DEBUG_INPUT) { @@ -640,11 +640,12 @@ public class ScreenshotController { // TODO(159460485): Remove this when focus is handled properly in the system setWindowFocusable(false); } - }, mFlags); - mScreenshotView.setDefaultDisplay(mDisplayId); - mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis()); + }); + mViewProxy.setFlags(mFlags); + mViewProxy.setDefaultDisplay(mDisplayId); + mViewProxy.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis()); - mScreenshotView.setOnKeyListener((v, keyCode, event) -> { + mViewProxy.setOnKeyListener((v, keyCode, event) -> { if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { if (DEBUG_INPUT) { Log.d(TAG, "onKeyEvent: " + keyCode); @@ -658,23 +659,24 @@ public class ScreenshotController { if (DEBUG_WINDOW) { Log.d(TAG, "adding OnComputeInternalInsetsListener"); } - mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView); + mViewProxy.getViewTreeObserver().addOnComputeInternalInsetsListener( + mViewProxy.getInternalInsetsListener()); if (DEBUG_WINDOW) { - Log.d(TAG, "setContentView: " + mScreenshotView); + Log.d(TAG, "setContentView: " + mViewProxy.getView()); } - setContentView(mScreenshotView); + setContentView(mViewProxy.getView()); } private void prepareAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) { - mScreenshotView.getViewTreeObserver().addOnPreDrawListener( + mViewProxy.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (DEBUG_WINDOW) { Log.d(TAG, "onPreDraw: startAnimation"); } - mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this); + mViewProxy.getViewTreeObserver().removeOnPreDrawListener(this); startAnimation(screenRect, showFlash, onAnimationComplete); return true; } @@ -694,13 +696,13 @@ public class ScreenshotController { if (mConfigChanges.applyNewConfig(mContext.getResources())) { // Hide the scroll chip until we know it's available in this // orientation - mScreenshotView.hideScrollChip(); + mViewProxy.hideScrollChip(); // Delay scroll capture eval a bit to allow the underlying activity // to set up in the new orientation. mScreenshotHandler.postDelayed(() -> { requestScrollCapture(owner); }, 150); - mScreenshotView.updateInsets( + mViewProxy.updateInsets( mWindowManager.getCurrentWindowMetrics().getWindowInsets()); // Screenshot animation calculations won't be valid anymore, // so just end @@ -759,16 +761,16 @@ public class ScreenshotController { + mLastScrollCaptureResponse.getWindowTitle() + "]"); final ScrollCaptureResponse response = mLastScrollCaptureResponse; - mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> { + mViewProxy.showScrollChip(response.getPackageName(), /* onClick */ () -> { DisplayMetrics displayMetrics = new DisplayMetrics(); getDisplay().getRealMetrics(displayMetrics); Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); - mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, + mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait); // delay starting scroll capture to make sure the scrim is up before the app moves - mScreenshotView.post(() -> runBatchScrollCapture(response, owner)); + mViewProxy.post(() -> runBatchScrollCapture(response, owner)); }); } catch (InterruptedException | ExecutionException e) { Log.e(TAG, "requestScrollCapture failed", e); @@ -794,19 +796,19 @@ public class ScreenshotController { return; } catch (InterruptedException | ExecutionException e) { Log.e(TAG, "Exception", e); - mScreenshotView.restoreNonScrollingUi(); + mViewProxy.restoreNonScrollingUi(); return; } if (longScreenshot.getHeight() == 0) { - mScreenshotView.restoreNonScrollingUi(); + mViewProxy.restoreNonScrollingUi(); return; } mLongScreenshotHolder.setLongScreenshot(longScreenshot); mLongScreenshotHolder.setTransitionDestinationCallback( (transitionDestination, onTransitionEnd) -> { - mScreenshotView.startLongScreenshotTransition( + mViewProxy.startLongScreenshotTransition( transitionDestination, onTransitionEnd, longScreenshot); // TODO: Do this via ActionIntentExecutor instead. @@ -882,16 +884,14 @@ public class ScreenshotController { } mWindowManager.removeViewImmediate(decorView); } - // Ensure that we remove the input monitor - if (mScreenshotView != null) { - mScreenshotView.stopInputListening(); - } + + mViewProxy.stopInputListening(); } private void playCameraSoundIfNeeded() { if (mScreenshotSoundController == null) return; // the controller is not-null only on the default display controller - mScreenshotSoundController.playCameraSound(); + mScreenshotSoundController.playScreenshotSoundAsync(); } /** @@ -932,7 +932,7 @@ public class ScreenshotController { } mScreenshotAnimation = - mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash); + mViewProxy.createScreenshotDropInAnimation(screenRect, showFlash); if (onAnimationComplete != null) { mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -975,7 +975,7 @@ public class ScreenshotController { }; Pair<ActivityOptions, ExitTransitionCoordinator> transition = ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null, - Pair.create(mScreenshotView.getScreenshotPreview(), + Pair.create(mViewProxy.getScreenshotPreview(), ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); return transition; @@ -999,7 +999,7 @@ public class ScreenshotController { mCurrentRequestCallback.onFinish(); mCurrentRequestCallback = null; } - mScreenshotView.reset(); + mViewProxy.reset(); removeWindow(); mScreenshotHandler.cancelTimeout(); } @@ -1067,7 +1067,7 @@ public class ScreenshotController { } private void doPostAnimation(ScreenshotController.SavedImageData imageData) { - mScreenshotView.setChipIntents(imageData); + mViewProxy.setChipIntents(imageData); } /** @@ -1084,11 +1084,11 @@ public class ScreenshotController { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); + mViewProxy.addQuickShareChip(quickShareData.quickShareAction); } }); } else { - mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); + mViewProxy.addQuickShareChip(quickShareData.quickShareAction); } }); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt index 2c0bddecc58e..d3a7fc4a3e4a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt @@ -21,22 +21,34 @@ import android.util.Log import com.android.app.tracing.coroutines.async import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.google.errorprone.annotations.CanIgnoreReturnValue import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout /** Controls sound reproduction after a screenshot is taken. */ interface ScreenshotSoundController { /** Reproduces the camera sound. */ - @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit> + suspend fun playScreenshotSound() - /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */ - @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit> + /** + * Releases the sound. [playScreenshotSound] behaviour is undefined after this has been called. + */ + suspend fun releaseScreenshotSound() + + /** Reproduces the camera sound. Used for compatibility with Java code. */ + fun playScreenshotSoundAsync() + + /** + * Releases the sound. [playScreenshotSound] behaviour is undefined after this has been called. + * Used for compatibility with Java code. + */ + fun releaseScreenshotSoundAsync() } class ScreenshotSoundControllerImpl @@ -47,8 +59,8 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher ) : ScreenshotSoundController { - val player: Deferred<MediaPlayer?> = - coroutineScope.async("loadCameraSound", bgDispatcher) { + private val player: Deferred<MediaPlayer?> = + coroutineScope.async("loadScreenshotSound", bgDispatcher) { try { soundProvider.getScreenshotSound() } catch (e: IllegalStateException) { @@ -57,8 +69,8 @@ constructor( } } - override fun playCameraSound(): Deferred<Unit> { - return coroutineScope.async("playCameraSound", bgDispatcher) { + override suspend fun playScreenshotSound() { + withContext(bgDispatcher) { try { player.await()?.start() } catch (e: IllegalStateException) { @@ -68,8 +80,8 @@ constructor( } } - override fun releaseScreenshotSound(): Deferred<Unit> { - return coroutineScope.async("releaseScreenshotSound", bgDispatcher) { + override suspend fun releaseScreenshotSound() { + withContext(bgDispatcher) { try { withTimeout(1.seconds) { player.await()?.release() } } catch (e: TimeoutCancellationException) { @@ -79,6 +91,14 @@ constructor( } } + override fun playScreenshotSoundAsync() { + coroutineScope.launch { playScreenshotSound() } + } + + override fun releaseScreenshotSoundAsync() { + coroutineScope.launch { releaseScreenshotSound() } + } + private companion object { const val TAG = "ScreenshotSoundControllerImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index be30a1576b21..8a8766dbab94 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -102,7 +102,7 @@ import java.util.ArrayList; public class ScreenshotView extends FrameLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { - interface ScreenshotViewCallback { + public interface ScreenshotViewCallback { void onUserInteraction(); void onAction(Intent intent, UserHandle owner, boolean overrideTransition); @@ -426,15 +426,15 @@ public class ScreenshotView extends FrameLayout implements return mScreenshotPreview; } - /** - * Set up the logger and callback on dismissal. - * - * Note: must be called before any other (non-constructor) method or null pointer exceptions - * may occur. - */ - void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks, FeatureFlags flags) { + void setUiEventLogger(UiEventLogger uiEventLogger) { mUiEventLogger = uiEventLogger; + } + + void setCallbacks(ScreenshotViewCallback callbacks) { mCallbacks = callbacks; + } + + void setFlags(FeatureFlags flags) { mFlags = flags; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt new file mode 100644 index 000000000000..0064521bd3a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.animation.Animator +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.View.OnKeyListener +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.flags.FeatureFlags + +/** Abstraction of the surface between ScreenshotController and ScreenshotView */ +interface ScreenshotViewProxy { + val view: ViewGroup + val internalInsetsListener: ViewTreeObserver.OnComputeInternalInsetsListener + val screenshotPreview: View + + var defaultDisplay: Int + var defaultTimeoutMillis: Long + var onKeyListener: OnKeyListener? + var flags: FeatureFlags? + var packageName: String + var logger: UiEventLogger? + var callbacks: ScreenshotView.ScreenshotViewCallback? + var screenshot: ScreenshotData? + + val isAttachedToWindow: Boolean + val isDismissing: Boolean + val isPendingSharedTransition: Boolean + + fun reset() + fun updateInsets(insets: WindowInsets) + fun updateOrientation(insets: WindowInsets) + fun badgeScreenshot(userBadgedIcon: Drawable) + fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator + fun addQuickShareChip(quickShareAction: Notification.Action) + fun setChipIntents(imageData: ScreenshotController.SavedImageData) + fun animateDismissal() + + fun showScrollChip(packageName: String, onClick: Runnable) + fun hideScrollChip() + fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean + ) + fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) + fun restoreNonScrollingUi() + + fun stopInputListening() + fun requestFocus() + fun announceForAccessibility(string: String) + fun addOnAttachStateChangeListener(listener: View.OnAttachStateChangeListener) + fun findOnBackInvokedDispatcher(): OnBackInvokedDispatcher? + fun getViewTreeObserver(): ViewTreeObserver + fun post(runnable: Runnable) + + interface Factory { + fun getProxy(context: Context): ScreenshotViewProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index bb34ede2cf5e..8a2678c8ab09 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -75,7 +75,7 @@ public class ScrollCaptureController { private String mWindowOwner; private volatile boolean mCancelled; - static class LongScreenshot { + public static class LongScreenshot { private final ImageTileSet mImageTileSet; private final Session mSession; // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index 3797b8b41e5a..a00c81d43b9e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -20,6 +20,7 @@ import android.app.Service; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; +import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.RequestProcessor; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; @@ -29,12 +30,14 @@ import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; import com.android.systemui.screenshot.ScreenshotSoundProviderImpl; +import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; import dagger.Binds; import dagger.Module; +import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; @@ -81,4 +84,9 @@ public abstract class ScreenshotModule { @Binds abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); + + @Provides + static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory() { + return new LegacyScreenshotViewProxy.Factory(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 1566de58ef3a..2fd438be9610 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -351,7 +351,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate(); private final NotificationGutsManager mGutsManager; private final AlternateBouncerInteractor mAlternateBouncerInteractor; - private final QuickSettingsController mQsController; + private final QuickSettingsControllerImpl mQsController; private final NaturalScrollingSettingObserver mNaturalScrollingSettingObserver; private final TouchHandler mTouchHandler = new TouchHandler(); @@ -726,7 +726,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump TapAgainViewController tapAgainViewController, NavigationModeController navigationModeController, NavigationBarController navigationBarController, - QuickSettingsController quickSettingsController, + QuickSettingsControllerImpl quickSettingsController, FragmentService fragmentService, IStatusBarService statusBarService, ContentResolver contentResolver, diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.kt new file mode 100644 index 000000000000..c96a339b560e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +interface QuickSettingsController { + /** Returns whether or not QuickSettings is expanded. */ + val expanded: Boolean + + /** Returns whether or not QuickSettings is being customized. */ + val isCustomizing: Boolean + + /** Returns Whether we should intercept a gesture to open Quick Settings. */ + @Deprecated("specific to legacy touch handling") + fun shouldQuickSettingsIntercept(x: Float, y: Float, yDiff: Float): Boolean + + /** Closes the Qs customizer. */ + fun closeQsCustomizer() + + /** + * This method closes QS but in split shade it should be used only in special cases: to make + * sure QS closes when shade is closed as well. Otherwise it will result in QS disappearing from + * split shade + */ + @Deprecated("specific to legacy split shade") fun closeQs() + + /** Calculate top padding for notifications */ + @Deprecated("specific to legacy DebugDrawable") + fun calculateNotificationsTopPadding( + isShadeExpanding: Boolean, + keyguardNotificationStaticPadding: Int, + expandedFraction: Float, + ): Float + + /** Calculate height of QS panel */ + @Deprecated("specific to legacy DebugDrawable") + fun calculatePanelHeightExpanded(stackScrollerPadding: Int): Int +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index a5c055350999..19d98a0bb83c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -118,7 +118,7 @@ import javax.inject.Inject; * TODO (b/264460656) make this dumpable */ @SysUISingleton -public class QuickSettingsController implements Dumpable { +public class QuickSettingsControllerImpl implements QuickSettingsController, Dumpable { public static final String TAG = "QuickSettingsController"; public static final int SHADE_BACK_ANIM_SCALE_MULTIPLIER = 100; @@ -252,7 +252,7 @@ public class QuickSettingsController implements Dumpable { /** * The window width currently in effect -- used together with - * {@link QuickSettingsController#mCachedGestureInsets} to decide whether a back gesture should + * {@link QuickSettingsControllerImpl#mCachedGestureInsets} to decide whether a back gesture should * receive a horizontal swipe inwards from the left/right vertical edge of the screen. * We cache this on ACTION_DOWN, and query it during both ACTION_DOWN and ACTION_MOVE events. */ @@ -304,7 +304,7 @@ public class QuickSettingsController implements Dumpable { private final QS.ScrollListener mQsScrollListener = this::onScroll; @Inject - public QuickSettingsController( + public QuickSettingsControllerImpl( Lazy<NotificationPanelViewController> panelViewControllerLazy, NotificationPanelView panelView, QsFrameTranslateController qsFrameTranslateController, @@ -399,23 +399,23 @@ public class QuickSettingsController implements Dumpable { mQs = qs; } - public void setExpansionHeightListener(ExpansionHeightListener listener) { + void setExpansionHeightListener(ExpansionHeightListener listener) { mExpansionHeightListener = listener; } - public void setQsStateUpdateListener(QsStateUpdateListener listener) { + void setQsStateUpdateListener(QsStateUpdateListener listener) { mQsStateUpdateListener = listener; } - public void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) { + void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) { mApplyClippingImmediatelyListener = listener; } - public void setFlingQsWithoutClickListener(FlingQsWithoutClickListener listener) { + void setFlingQsWithoutClickListener(FlingQsWithoutClickListener listener) { mFlingQsWithoutClickListener = listener; } - public void setExpansionHeightSetToMaxListener(ExpansionHeightSetToMaxListener callback) { + void setExpansionHeightSetToMaxListener(ExpansionHeightSetToMaxListener callback) { mExpansionHeightSetToMaxListener = callback; } @@ -507,15 +507,11 @@ public class QuickSettingsController implements Dumpable { && mPanelView.getRootWindowInsets().isVisible(ime()); } - public boolean isExpansionEnabled() { + boolean isExpansionEnabled() { return mExpansionEnabledPolicy && mExpansionEnabledAmbient && !isRemoteInputActiveWithKeyboardUp(); } - public float getTransitioningToFullShadeProgress() { - return mTransitioningToFullShadeProgress; - } - /** */ @VisibleForTesting boolean isExpandImmediate() { @@ -536,7 +532,7 @@ public class QuickSettingsController implements Dumpable { * Computes (and caches) the gesture insets for the current window. Intended to be called * on ACTION_DOWN, and safely queried repeatedly thereafter during ACTION_MOVE events. */ - public void updateGestureInsetsCache() { + void updateGestureInsetsCache() { WindowManager wm = this.mPanelView.getContext().getSystemService(WindowManager.class); WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); mCachedGestureInsets = windowMetrics.getWindowInsets().getInsets( @@ -548,7 +544,7 @@ public class QuickSettingsController implements Dumpable { * Returns whether x coordinate lies in the vertical edges of the screen * (the only place where a back gesture can be initiated). */ - public boolean shouldBackBypassQuickSettings(float touchX) { + boolean shouldBackBypassQuickSettings(float touchX) { return (touchX < mCachedGestureInsets.left) || (touchX > mCachedWindowWidth - mCachedGestureInsets.right); } @@ -592,6 +588,7 @@ public class QuickSettingsController implements Dumpable { return twoFingerDrag || stylusButtonClickDrag || mouseButtonClickDrag; } + @Override public boolean getExpanded() { return mShadeRepository.getLegacyIsQsExpanded().getValue(); } @@ -601,7 +598,7 @@ public class QuickSettingsController implements Dumpable { return mShadeRepository.getLegacyQsTracking().getValue(); } - public boolean getFullyExpanded() { + boolean getFullyExpanded() { return mFullyExpanded; } @@ -623,27 +620,28 @@ public class QuickSettingsController implements Dumpable { return mQs != null; } + @Override public boolean isCustomizing() { return isQsFragmentCreated() && mQs.isCustomizing(); } - public float getExpansionHeight() { + float getExpansionHeight() { return mExpansionHeight; } - public boolean getExpandedWhenExpandingStarted() { + boolean getExpandedWhenExpandingStarted() { return mExpandedWhenExpandingStarted; } - public int getMinExpansionHeight() { + int getMinExpansionHeight() { return mMinExpansionHeight; } - public boolean isFullyExpandedAndTouchesDisallowed() { + boolean isFullyExpandedAndTouchesDisallowed() { return isQsFragmentCreated() && getFullyExpanded() && disallowTouches(); } - public int getMaxExpansionHeight() { + int getMaxExpansionHeight() { return mMaxExpansionHeight; } @@ -654,13 +652,14 @@ public class QuickSettingsController implements Dumpable { return !mTouchAboveFalsingThreshold; } - public int getFalsingThreshold() { + int getFalsingThreshold() { return mFalsingThreshold; } /** * Returns Whether we should intercept a gesture to open Quick Settings. */ + @Override public boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { boolean keyguardShowing = mBarState == KEYGUARD; if (!isExpansionEnabled() || mCollapsedOnDown || (keyguardShowing @@ -717,7 +716,7 @@ public class QuickSettingsController implements Dumpable { * @param downY the y location where the touch started * Returns true if the panel could be collapsed because it stared on QQS */ - public boolean canPanelCollapseOnQQS(float downX, float downY) { + boolean canPanelCollapseOnQQS(float downX, float downY) { if (mCollapsedOnDown || mBarState == KEYGUARD || getExpanded()) { return false; } @@ -727,6 +726,7 @@ public class QuickSettingsController implements Dumpable { } /** Closes the Qs customizer. */ + @Override public void closeQsCustomizer() { if (mQs != null) { mQs.closeCustomizer(); @@ -734,7 +734,7 @@ public class QuickSettingsController implements Dumpable { } /** Returns whether touches from the notification panel should be disallowed */ - public boolean disallowTouches() { + boolean disallowTouches() { if (mQs != null) { return mQs.disallowPanelTouches(); } else { @@ -754,15 +754,11 @@ public class QuickSettingsController implements Dumpable { } } - public void setDozing(boolean dozing) { + void setDozing(boolean dozing) { mDozing = dozing; } - /** - * This method closes QS but in split shade it should be used only in special cases: to make - * sure QS closes when shade is closed as well. Otherwise it will result in QS disappearing - * from split shade - */ + @Override public void closeQs() { if (mSplitShadeEnabled) { mShadeLog.d("Closing QS while in split shade"); @@ -794,7 +790,7 @@ public class QuickSettingsController implements Dumpable { } /** update Qs height state */ - public void setExpansionHeight(float height) { + void setExpansionHeight(float height) { int maxHeight = getMaxExpansionHeight(); height = Math.min(Math.max( height, getMinExpansionHeight()), maxHeight); @@ -817,7 +813,7 @@ public class QuickSettingsController implements Dumpable { } /** */ - public void setHeightOverrideToDesiredHeight() { + void setHeightOverrideToDesiredHeight() { if (isSizeChangeAnimationRunning() && isQsFragmentCreated()) { mQs.setHeightOverride(mQs.getDesiredHeight()); } @@ -919,7 +915,7 @@ public class QuickSettingsController implements Dumpable { } /** Sets Qs ScrimEnabled and updates QS state. */ - public void setScrimEnabled(boolean scrimEnabled) { + void setScrimEnabled(boolean scrimEnabled) { boolean changed = mScrimEnabled != scrimEnabled; mScrimEnabled = scrimEnabled; if (changed) { @@ -995,7 +991,7 @@ public class QuickSettingsController implements Dumpable { } /** update expanded state of QS */ - public void updateExpansion() { + void updateExpansion() { if (mQs == null) return; final float squishiness; if ((isExpandImmediate() || getExpanded()) && !mSplitShadeEnabled) { @@ -1053,13 +1049,13 @@ public class QuickSettingsController implements Dumpable { // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade // transition. If that's not the case we should follow QS expansion fraction for when // user is pulling from the same top to go directly to expanded QS - return getTransitioningToFullShadeProgress() > 0 + return mTransitioningToFullShadeProgress > 0 ? mLockscreenShadeTransitionController.getQSDragProgress() : computeExpansionFraction(); } /** */ - public void updateExpansionEnabledAmbient() { + void updateExpansionEnabledAmbient() { final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsHeaderHeight; mExpansionEnabledAmbient = mSplitShadeEnabled || (mAmbientState.getScrollY() <= scrollRangeToTop); @@ -1081,7 +1077,7 @@ public class QuickSettingsController implements Dumpable { } /** Calculate fraction of current QS expansion state */ - public float computeExpansionFraction() { + float computeExpansionFraction() { if (mAnimatingHiddenFromCollapsed) { // When hiding QS from collapsed state, the expansion can sometimes temporarily // be larger than 0 because of the timing, leading to flickers. @@ -1112,7 +1108,7 @@ public class QuickSettingsController implements Dumpable { } /** Called when shade starts expanding. */ - public void onExpandingStarted(boolean qsFullyExpanded) { + void onExpandingStarted(boolean qsFullyExpanded) { mNotificationStackScrollLayoutController.onExpansionStarted(); mExpandedWhenExpandingStarted = qsFullyExpanded; mMediaHierarchyManager.setCollapsingShadeFromQS(mExpandedWhenExpandingStarted @@ -1363,7 +1359,7 @@ public class QuickSettingsController implements Dumpable { } } - /** Calculate top padding for notifications */ + @Override public float calculateNotificationsTopPadding(boolean isShadeExpanding, int keyguardNotificationStaticPadding, float expandedFraction) { float topPadding; @@ -1404,7 +1400,7 @@ public class QuickSettingsController implements Dumpable { } } - /** Calculate height of QS panel */ + @Override public int calculatePanelHeightExpanded(int stackScrollerPadding) { float notificationHeight = @@ -1576,7 +1572,6 @@ public class QuickSettingsController implements Dumpable { } } - @VisibleForTesting boolean isTrackingBlocked() { return mConflictingExpansionGesture && getExpanded(); } @@ -1591,7 +1586,7 @@ public class QuickSettingsController implements Dumpable { } /** handles touches in Qs panel area */ - public boolean handleTouch(MotionEvent event, boolean isFullyCollapsed, + boolean handleTouch(MotionEvent event, boolean isFullyCollapsed, boolean isShadeOrQsHeightAnimationRunning) { if (isSplitShadeAndTouchXOutsideQs(event.getX())) { return false; @@ -1747,7 +1742,7 @@ public class QuickSettingsController implements Dumpable { } /** intercepts touches on Qs panel area. */ - public boolean onIntercept(MotionEvent event) { + boolean onIntercept(MotionEvent event) { int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; @@ -1858,7 +1853,7 @@ public class QuickSettingsController implements Dumpable { * * @param animateAway Do not stop when QS becomes QQS. Fling until QS isn't visible anymore. */ - public void animateCloseQs(boolean animateAway) { + void animateCloseQs(boolean animateAway) { if (mExpansionAnimator != null) { if (!mAnimatorExpand) { return; @@ -1877,7 +1872,7 @@ public class QuickSettingsController implements Dumpable { } /** @see #flingQs(float, int, Runnable, boolean) */ - public void flingQs(float vel, int type) { + void flingQs(float vel, int type) { flingQs(vel, type, null /* onFinishRunnable */, false /* isClick */); } @@ -2144,7 +2139,7 @@ public class QuickSettingsController implements Dumpable { } /** */ - public FragmentHostManager.FragmentListener getQsFragmentListener() { + FragmentHostManager.FragmentListener getQsFragmentListener() { return new QsFragmentListener(); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt new file mode 100644 index 000000000000..b8250cc284bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.qs.QSContainerController +import com.android.systemui.qs.ui.adapter.QSSceneAdapter +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import javax.inject.Inject + +@SysUISingleton +class QuickSettingsControllerSceneImpl +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + private val qsSceneAdapter: QSSceneAdapter, + private val qsContainerController: QSContainerController, +) : QuickSettingsController { + + override val expanded: Boolean + get() = shadeInteractor.isQsExpanded.value + + override val isCustomizing: Boolean + get() = qsSceneAdapter.isCustomizing.value + + @Deprecated("specific to legacy touch handling") + override fun shouldQuickSettingsIntercept(x: Float, y: Float, yDiff: Float): Boolean { + throw UnsupportedOperationException() + } + + override fun closeQsCustomizer() { + qsContainerController.setCustomizerShowing(false) + } + + @Deprecated("specific to legacy split shade") + override fun closeQs() { + // Do nothing + } + + @Deprecated("specific to legacy DebugDrawable") + override fun calculateNotificationsTopPadding( + isShadeExpanding: Boolean, + keyguardNotificationStaticPadding: Int, + expandedFraction: Float + ): Float { + throw UnsupportedOperationException() + } + + @Deprecated("specific to legacy DebugDrawable") + override fun calculatePanelHeightExpanded(stackScrollerPadding: Int): Int { + throw UnsupportedOperationException() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 5632766f2633..504dbfdafd0b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -17,6 +17,8 @@ package com.android.systemui.shade import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.qs.QSContainerController +import com.android.systemui.qs.ui.adapter.QSSceneAdapterImpl import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.data.repository.PrivacyChipRepository import com.android.systemui.shade.data.repository.PrivacyChipRepositoryImpl @@ -111,6 +113,26 @@ abstract class ShadeModule { sceneContainerOff.get() } } + + @Provides + @SysUISingleton + fun provideQuickSettingsController( + sceneContainerFlags: SceneContainerFlags, + sceneContainerOn: Provider<QuickSettingsControllerSceneImpl>, + sceneContainerOff: Provider<QuickSettingsControllerImpl>, + ): QuickSettingsController { + return if (sceneContainerFlags.isEnabled()) { + sceneContainerOn.get() + } else { + sceneContainerOff.get() + } + } + + @Provides + @SysUISingleton + fun providesQSContainerController(impl: QSSceneAdapterImpl): QSContainerController { + return impl + } } @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index ef4e5308e18e..a12b9709a063 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -84,6 +84,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -457,14 +458,43 @@ public final class KeyboardShortcutListSearch { List<KeyboardShortcutMultiMappingGroup> keyboardShortcutMultiMappingGroups = new ArrayList<>(); for (KeyboardShortcutGroup group : keyboardShortcutGroups) { - CharSequence categoryTitle = group.getLabel(); - List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>(); + KeyboardShortcutMultiMappingGroup mappedGroup = + new KeyboardShortcutMultiMappingGroup( + group.getLabel(), + new ArrayList<>()); + Map<String, List<ShortcutMultiMappingInfo>> shortcutMap = new LinkedHashMap<>(); for (KeyboardShortcutInfo info : group.getItems()) { - shortcutMultiMappingInfos.add(new ShortcutMultiMappingInfo(info)); + String label = info.getLabel().toString(); + Icon icon = info.getIcon(); + if (shortcutMap.containsKey(label)) { + List<ShortcutMultiMappingInfo> shortcuts = shortcutMap.get(label); + boolean foundSameIcon = false; + for (ShortcutMultiMappingInfo shortcut : shortcuts) { + Icon shortcutIcon = shortcut.getIcon(); + if ((shortcutIcon != null + && icon != null + && shortcutIcon.sameAs(icon)) + || (shortcutIcon == null && icon == null)) { + foundSameIcon = true; + shortcut.addShortcutKeyGroup(new ShortcutKeyGroup(info, null)); + break; + } + } + if (!foundSameIcon) { + shortcuts.add(new ShortcutMultiMappingInfo(info)); + } + } else { + List<ShortcutMultiMappingInfo> shortcuts = new ArrayList<>(); + shortcuts.add(new ShortcutMultiMappingInfo(info)); + shortcutMap.put(label, shortcuts); + } } - keyboardShortcutMultiMappingGroups.add( - new KeyboardShortcutMultiMappingGroup( - categoryTitle, shortcutMultiMappingInfos)); + for (List<ShortcutMultiMappingInfo> shortcutInfos : shortcutMap.values()) { + for (ShortcutMultiMappingInfo shortcutInfo : shortcutInfos) { + mappedGroup.addItem(shortcutInfo); + } + } + keyboardShortcutMultiMappingGroups.add(mappedGroup); } return keyboardShortcutMultiMappingGroups; } @@ -1377,7 +1407,9 @@ public final class KeyboardShortcutListSearch { ShortcutMultiMappingInfo(KeyboardShortcutInfo info) { mLabel = info.getLabel(); mIcon = info.getIcon(); - mShortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(info, null)); + mShortcutKeyGroups = new ArrayList<>( + Arrays.asList(new ShortcutKeyGroup(info, null)) + ); } CharSequence getLabel() { @@ -1388,6 +1420,10 @@ public final class KeyboardShortcutListSearch { return mIcon; } + void addShortcutKeyGroup(ShortcutKeyGroup group) { + mShortcutKeyGroups.add(group); + } + List<ShortcutKeyGroup> getShortcutKeyGroups() { return mShortcutKeyGroups; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt new file mode 100644 index 000000000000..1bebbfd34ff4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/StatusBarKeyguardViewManagerInteractor.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** + * Whether to set the status bar keyguard view occluded or not, and whether to animate that change. + */ +data class OccludedState( + val occluded: Boolean, + val animate: Boolean = false, +) + +/** Handles logic around calls to [StatusBarKeyguardViewManager] in legacy code. */ +@Deprecated("Will be removed once all of SBKVM's responsibilies are refactored.") +@SysUISingleton +class StatusBarKeyguardViewManagerInteractor +@Inject +constructor( + keyguardTransitionInteractor: KeyguardTransitionInteractor, + keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + powerInteractor: PowerInteractor, +) { + /** Occlusion state to apply whenever a keyguard transition is STARTED, if any. */ + private val occlusionStateFromStartedStep: Flow<OccludedState> = + keyguardTransitionInteractor.startedKeyguardTransitionStep + .sample(powerInteractor.detailedWakefulness, ::Pair) + .map { (startedStep, wakefulness) -> + val transitioningFromPowerButtonGesture = + KeyguardState.deviceIsAsleepInState(startedStep.from) && + startedStep.to == KeyguardState.OCCLUDED && + wakefulness.powerButtonLaunchGestureTriggered + + if ( + startedStep.to == KeyguardState.OCCLUDED && !transitioningFromPowerButtonGesture + ) { + // Set occluded upon STARTED, *unless* we're transitioning from the power + // button, in which case we're going to play an animation over the lockscreen UI + // and need to remain unoccluded until the transition finishes. + return@map OccludedState(occluded = true, animate = false) + } + + if ( + startedStep.from == KeyguardState.OCCLUDED && + startedStep.to == KeyguardState.LOCKSCREEN + ) { + // Set unoccluded immediately ONLY if we're transitioning back to the lockscreen + // since we need the views visible to animate them back down. This is a special + // case due to the way unocclusion remote animations are run. We can remove this + // once the unocclude animation uses the return animation framework. + return@map OccludedState(occluded = false, animate = false) + } + + // Otherwise, wait for the transition to FINISH to decide. + return@map null + } + .filterNotNull() + + /** Occlusion state to apply whenever a keyguard transition is FINISHED. */ + private val occlusionStateFromFinishedStep = + keyguardTransitionInteractor.finishedKeyguardTransitionStep + .sample(keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop, ::Pair) + .map { (finishedStep, showWhenLockedOnTop) -> + // If we're FINISHED in OCCLUDED, we want to render as occluded. We also need to + // remain occluded if a SHOW_WHEN_LOCKED activity is on the top of the task stack, + // and we're in any state other than GONE. This is necessary, for example, when we + // transition from OCCLUDED to a bouncer state. Otherwise, we should not be + // occluded. + val occluded = + finishedStep.to == KeyguardState.OCCLUDED || + (showWhenLockedOnTop && finishedStep.to != KeyguardState.GONE) + OccludedState(occluded = occluded, animate = false) + } + + /** Occlusion state to apply to SKBVM's setOccluded call. */ + val keyguardViewOcclusionState = + merge(occlusionStateFromStartedStep, occlusionStateFromFinishedStep) + .distinctUntilChangedBy { + // Don't switch 'animate' values mid-transition. + it.occluded + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt index dab89c5235b0..b90aa107d617 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt @@ -19,7 +19,9 @@ package com.android.systemui.statusbar.notification.row import android.widget.flags.Flags.notifLinearlayoutOptimized import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing import javax.inject.Inject +import javax.inject.Provider interface NotifRemoteViewsFactoryContainer { val factories: Set<NotifRemoteViewsFactory> @@ -31,7 +33,8 @@ constructor( featureFlags: FeatureFlags, precomputedTextViewFactory: PrecomputedTextViewFactory, bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory, - optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory + optimizedLinearLayoutFactory: NotificationOptimizedLinearLayoutFactory, + notificationViewFlipperFactory: Provider<NotificationViewFlipperFactory>, ) : NotifRemoteViewsFactoryContainer { override val factories: Set<NotifRemoteViewsFactory> = buildSet { add(precomputedTextViewFactory) @@ -41,5 +44,8 @@ constructor( if (notifLinearlayoutOptimized()) { add(optimizedLinearLayoutFactory) } + if (NotificationViewFlipperPausing.isEnabled) { + add(notificationViewFlipperFactory.get()) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index d308fa583f71..c17ee3991713 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -833,143 +833,143 @@ public class NotificationContentInflater implements NotificationRowContentBinder @Nullable InflationCallback endListener, NotificationEntry entry, ExpandableNotificationRow row, NotificationContentInflaterLogger logger) { Assert.isMainThread(); + if (!runningInflations.isEmpty()) { + return false; + } NotificationContentView privateLayout = row.getPrivateLayout(); NotificationContentView publicLayout = row.getPublicLayout(); - if (runningInflations.isEmpty()) { - logger.logAsyncTaskProgress(entry, "finishing"); - boolean setRepliesAndActions = true; - if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { - if (result.inflatedContentView != null) { - // New view case - privateLayout.setContractedChild(result.inflatedContentView); - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, - result.newContentView); - } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) { - // Reinflation case. Only update if it's still cached (i.e. view has not been - // freed while inflating). - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, - result.newContentView); - } - setRepliesAndActions = true; + logger.logAsyncTaskProgress(entry, "finishing"); + boolean setRepliesAndActions = true; + if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { + if (result.inflatedContentView != null) { + // New view case + privateLayout.setContractedChild(result.inflatedContentView); + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, + result.newContentView); + } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) { + // Reinflation case. Only update if it's still cached (i.e. view has not been + // freed while inflating). + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, + result.newContentView); } + setRepliesAndActions = true; + } - if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { - if (result.inflatedExpandedView != null) { - privateLayout.setExpandedChild(result.inflatedExpandedView); - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, - result.newExpandedView); - } else if (result.newExpandedView == null) { - privateLayout.setExpandedChild(null); - remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); - } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) { - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, - result.newExpandedView); - } - if (result.newExpandedView != null) { - privateLayout.setExpandedInflatedSmartReplies( - result.expandedInflatedSmartReplies); - } else { - privateLayout.setExpandedInflatedSmartReplies(null); - } - row.setExpandable(result.newExpandedView != null); - setRepliesAndActions = true; + if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { + if (result.inflatedExpandedView != null) { + privateLayout.setExpandedChild(result.inflatedExpandedView); + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, + result.newExpandedView); + } else if (result.newExpandedView == null) { + privateLayout.setExpandedChild(null); + remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); + } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) { + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, + result.newExpandedView); } + if (result.newExpandedView != null) { + privateLayout.setExpandedInflatedSmartReplies( + result.expandedInflatedSmartReplies); + } else { + privateLayout.setExpandedInflatedSmartReplies(null); + } + row.setExpandable(result.newExpandedView != null); + setRepliesAndActions = true; + } - if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { - if (result.inflatedHeadsUpView != null) { - privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, - result.newHeadsUpView); - } else if (result.newHeadsUpView == null) { - privateLayout.setHeadsUpChild(null); - remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); - } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) { - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, - result.newHeadsUpView); - } - if (result.newHeadsUpView != null) { - privateLayout.setHeadsUpInflatedSmartReplies( - result.headsUpInflatedSmartReplies); - } else { - privateLayout.setHeadsUpInflatedSmartReplies(null); - } - setRepliesAndActions = true; + if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { + if (result.inflatedHeadsUpView != null) { + privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, + result.newHeadsUpView); + } else if (result.newHeadsUpView == null) { + privateLayout.setHeadsUpChild(null); + remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); + } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) { + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, + result.newHeadsUpView); } + if (result.newHeadsUpView != null) { + privateLayout.setHeadsUpInflatedSmartReplies( + result.headsUpInflatedSmartReplies); + } else { + privateLayout.setHeadsUpInflatedSmartReplies(null); + } + setRepliesAndActions = true; + } - if (AsyncHybridViewInflation.isEnabled() - && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { - HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder; - SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel; - if (viewHolder != null && viewModel != null) { - if (viewModel.isConversation()) { - SingleLineConversationViewBinder.bind( - result.mInflatedSingleLineViewModel, - result.mInflatedSingleLineViewHolder - ); - } else { - SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel, - result.mInflatedSingleLineViewHolder); - } - privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder); + if (AsyncHybridViewInflation.isEnabled() + && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { + HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder; + SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel; + if (viewHolder != null && viewModel != null) { + if (viewModel.isConversation()) { + SingleLineConversationViewBinder.bind( + result.mInflatedSingleLineViewModel, + result.mInflatedSingleLineViewHolder + ); + } else { + SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel, + result.mInflatedSingleLineViewHolder); } + privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder); } + } - if (setRepliesAndActions) { - privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); - } + if (setRepliesAndActions) { + privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); + } - if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { - if (result.inflatedPublicView != null) { - publicLayout.setContractedChild(result.inflatedPublicView); - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, - result.newPublicView); - } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) { - remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, - result.newPublicView); - } + if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { + if (result.inflatedPublicView != null) { + publicLayout.setContractedChild(result.inflatedPublicView); + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, + result.newPublicView); + } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) { + remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, + result.newPublicView); } + } - if (AsyncGroupHeaderViewInflation.isEnabled()) { - if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { - if (result.mInflatedGroupHeaderView != null) { - row.setIsLowPriority(false); - row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView); - remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, - result.mNewGroupHeaderView); - } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) { - // Re-inflation case. Only update if it's still cached (i.e. view has not - // been freed while inflating). - remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, - result.mNewGroupHeaderView); - } + if (AsyncGroupHeaderViewInflation.isEnabled()) { + if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { + if (result.mInflatedGroupHeaderView != null) { + row.setIsLowPriority(false); + row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView); + remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, + result.mNewGroupHeaderView); + } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) { + // Re-inflation case. Only update if it's still cached (i.e. view has not + // been freed while inflating). + remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, + result.mNewGroupHeaderView); } + } - if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { - if (result.mInflatedLowPriorityGroupHeaderView != null) { - // New view case, set row to low priority - row.setIsLowPriority(true); - row.setLowPriorityGroupHeader( - /* headerView= */ result.mInflatedLowPriorityGroupHeaderView); - remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, - result.mNewLowPriorityGroupHeaderView); - } else if (remoteViewCache.hasCachedView(entry, - FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) { - // Re-inflation case. Only update if it's still cached (i.e. view has not - // been freed while inflating). - remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, - result.mNewGroupHeaderView); - } + if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { + if (result.mInflatedLowPriorityGroupHeaderView != null) { + // New view case, set row to low priority + row.setIsLowPriority(true); + row.setLowPriorityGroupHeader( + /* headerView= */ result.mInflatedLowPriorityGroupHeaderView); + remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, + result.mNewLowPriorityGroupHeaderView); + } else if (remoteViewCache.hasCachedView(entry, + FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) { + // Re-inflation case. Only update if it's still cached (i.e. view has not + // been freed while inflating). + remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, + result.mNewGroupHeaderView); } } + } - entry.headsUpStatusBarText = result.headsUpStatusBarText; - entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; - if (endListener != null) { - endListener.onAsyncInflationFinished(entry); - } - return true; + entry.headsUpStatusBarText = result.headsUpStatusBarText; + entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; + if (endListener != null) { + endListener.onAsyncInflationFinished(entry); } - return false; + return true; } private static RemoteViews createExpandedView(Notification.Builder builder, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt new file mode 100644 index 000000000000..0594c1227a44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationViewFlipperFactory.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ViewFlipper +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag +import com.android.systemui.statusbar.notification.row.ui.viewbinder.NotificationViewFlipperBinder +import com.android.systemui.statusbar.notification.row.ui.viewmodel.NotificationViewFlipperViewModel +import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing +import javax.inject.Inject + +/** + * A factory which owns the construction of any ViewFlipper inside of Notifications, and binds it + * with a view model. This ensures that ViewFlippers are paused when the keyguard is showing. + */ +class NotificationViewFlipperFactory +@Inject +constructor( + private val viewModel: NotificationViewFlipperViewModel, +) : NotifRemoteViewsFactory { + init { + /* check if */ NotificationViewFlipperPausing.isUnexpectedlyInLegacyMode() + } + + override fun instantiate( + row: ExpandableNotificationRow, + @InflationFlag layoutType: Int, + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + return when (name) { + ViewFlipper::class.java.name, + ViewFlipper::class.java.simpleName -> + ViewFlipper(context, attrs).also { viewFlipper -> + NotificationViewFlipperBinder.bindWhileAttached(viewFlipper, viewModel) + } + else -> null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java index 9445d56ab509..ea3036e35c1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java @@ -31,6 +31,7 @@ import androidx.asynclayoutinflater.view.AsyncLayoutInflater; import com.android.systemui.res.R; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.util.time.SystemClock; import javax.inject.Inject; @@ -46,9 +47,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private NotificationEntry mEntry; private boolean mCancelled; private Throwable mInflateOrigin; + private final SystemClock mSystemClock; + private final RowInflaterTaskLogger mLogger; + private long mInflateStartTimeMs; @Inject - public RowInflaterTask() { + public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) { + mSystemClock = systemClock; + mLogger = logger; } /** @@ -61,29 +67,49 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } mListener = listener; AsyncLayoutInflater inflater = com.android.systemui.Flags.notificationRowUserContext() - ? new AsyncLayoutInflater(context, new RowAsyncLayoutInflater(entry)) + ? new AsyncLayoutInflater(context, makeRowInflater(entry)) : new AsyncLayoutInflater(context); mEntry = entry; entry.setInflationTask(this); + + mLogger.logInflateStart(entry); + mInflateStartTimeMs = mSystemClock.elapsedRealtime(); inflater.inflate(R.layout.status_bar_notification_row, parent, this); } + private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) { + return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger); + } + @VisibleForTesting static class RowAsyncLayoutInflater implements AsyncLayoutFactory { private final NotificationEntry mEntry; + private final SystemClock mSystemClock; + private final RowInflaterTaskLogger mLogger; - RowAsyncLayoutInflater(NotificationEntry entry) { + RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock, + RowInflaterTaskLogger logger) { mEntry = entry; + mSystemClock = systemClock; + mLogger = logger; } @Nullable @Override public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { - if (name.equals(ExpandableNotificationRow.class.getName())) { - return new ExpandableNotificationRow(context, attrs, mEntry); + if (!name.equals(ExpandableNotificationRow.class.getName())) { + return null; } - return null; + + final long startMs = mSystemClock.elapsedRealtime(); + final ExpandableNotificationRow row = + new ExpandableNotificationRow(context, attrs, mEntry); + final long elapsedMs = mSystemClock.elapsedRealtime() - startMs; + + mLogger.logCreatedRow(mEntry, elapsedMs); + + return row; } @Nullable @@ -101,6 +127,9 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf @Override public void onInflateFinished(View view, int resid, ViewGroup parent) { + final long elapsedMs = mSystemClock.elapsedRealtime() - mInflateStartTimeMs; + mLogger.logInflateFinish(mEntry, elapsedMs, mCancelled); + if (!mCancelled) { try { mEntry.onInflationTaskFinished(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTaskLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTaskLogger.kt new file mode 100644 index 000000000000..94da6bec799b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTaskLogger.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.NotifInflationLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey +import javax.inject.Inject + +class RowInflaterTaskLogger @Inject constructor(@NotifInflationLog private val buffer: LogBuffer) { + fun logInflateStart(entry: NotificationEntry) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = entry.logKey }, + { "started row inflation for $str1" } + ) + } + + fun logCreatedRow(entry: NotificationEntry, elapsedMs: Long) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = entry.logKey + long1 = elapsedMs + }, + { "created row in $long1 ms for $str1" } + ) + } + + fun logInflateFinish(entry: NotificationEntry, elapsedMs: Long, cancelled: Boolean) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = entry.logKey + long1 = elapsedMs + bool1 = cancelled + }, + { "finished ${if (bool1) "cancelled " else ""}row inflation in $long1 ms for $str1" } + ) + } +} + +private const val TAG = "RowInflaterTask" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt new file mode 100644 index 000000000000..133d3e730eff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/NotificationViewFlipperBinder.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.viewbinder + +import android.widget.ViewFlipper +import androidx.lifecycle.lifecycleScope +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.row.ui.viewmodel.NotificationViewFlipperViewModel +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +/** Binds a [NotificationViewFlipper] to its [view model][NotificationViewFlipperViewModel]. */ +object NotificationViewFlipperBinder { + fun bindWhileAttached( + viewFlipper: ViewFlipper, + viewModel: NotificationViewFlipperViewModel, + ): DisposableHandle { + if (viewFlipper.isAutoStart) { + // If the ViewFlipper is not set to AutoStart, the pause binding is meaningless + return DisposableHandle {} + } + return viewFlipper.repeatWhenAttached { + lifecycleScope.launch { bind(viewFlipper, viewModel) } + } + } + + suspend fun bind( + viewFlipper: ViewFlipper, + viewModel: NotificationViewFlipperViewModel, + ) = coroutineScope { launch { viewModel.isPaused.collect { viewFlipper.setPaused(it) } } } + + private fun ViewFlipper.setPaused(paused: Boolean) { + if (paused) { + stopFlipping() + } else if (isAutoStart) { + startFlipping() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt new file mode 100644 index 000000000000..7694e58296ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModel.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing +import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor +import com.android.systemui.util.kotlin.FlowDumperImpl +import javax.inject.Inject + +/** A model which represents whether ViewFlippers inside notifications should be paused. */ +@SysUISingleton +class NotificationViewFlipperViewModel +@Inject +constructor( + dumpManager: DumpManager, + stackInteractor: NotificationStackInteractor, +) : FlowDumperImpl(dumpManager) { + init { + /* check if */ NotificationViewFlipperPausing.isUnexpectedlyInLegacyMode() + } + + val isPaused = stackInteractor.isShowingOnLockscreen.dumpWhileCollecting("isPaused") +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt new file mode 100644 index 000000000000..cea6a2b197be --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationViewFlipperPausing.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notification view flipper pausing flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationViewFlipperPausing { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_VIEW_FLIPPER_PAUSING + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationViewFlipperPausing() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index f5237938ebfa..78fc1471053d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -42,8 +42,10 @@ import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel @@ -99,7 +101,9 @@ constructor( private val alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, + private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, + private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, private val glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, @@ -155,8 +159,8 @@ constructor( .distinctUntilChanged() .dumpWhileCollecting("isShadeLocked") - private val shadeCollapseFadeInComplete = MutableStateFlow(false) - .dumpValue("shadeCollapseFadeInComplete") + private val shadeCollapseFadeInComplete = + MutableStateFlow(false).dumpValue("shadeCollapseFadeInComplete") val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = interactor.configurationBasedDimensions @@ -187,9 +191,8 @@ constructor( statesForConstrainedNotifications.contains(it) }, keyguardTransitionInteractor - .transitionValue(LOCKSCREEN) - .onStart { emit(0f) } - .map { it > 0 } + .isInTransitionWhere { from, to -> from == LOCKSCREEN || to == LOCKSCREEN } + .onStart { emit(false) } ) { constrainedNotificationState, transitioningToOrFromLockscreen -> constrainedNotificationState || transitioningToOrFromLockscreen } @@ -362,16 +365,16 @@ constructor( private val alphaWhenGoneAndShadeState: Flow<Float> = combineTransform( - keyguardTransitionInteractor.transitions - .map { step -> step.to == GONE && step.transitionState == FINISHED } - .distinctUntilChanged(), - keyguardInteractor.statusBarState, - ) { isGoneTransitionFinished, statusBarState -> - if (isGoneTransitionFinished && statusBarState == SHADE) { - emit(1f) + keyguardTransitionInteractor.transitions + .map { step -> step.to == GONE && step.transitionState == FINISHED } + .distinctUntilChanged(), + keyguardInteractor.statusBarState, + ) { isGoneTransitionFinished, statusBarState -> + if (isGoneTransitionFinished && statusBarState == SHADE) { + emit(1f) + } } - } - .dumpWhileCollecting("alphaWhenGoneAndShadeState") + .dumpWhileCollecting("alphaWhenGoneAndShadeState") fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> { // All transition view models are mututally exclusive, and safe to merge @@ -379,7 +382,9 @@ constructor( merge( alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, aodToLockscreenTransitionViewModel.notificationAlpha, + aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, + dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dreamingToLockscreenTransitionViewModel.lockscreenAlpha, goneToAodTransitionViewModel.notificationAlpha, goneToDreamingTransitionViewModel.lockscreenAlpha, @@ -433,30 +438,35 @@ constructor( * alpha sources. */ val glanceableHubAlpha: Flow<Float> = - isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade -> - combineTransform( - lockscreenToGlanceableHubRunning, - glanceableHubToLockscreenRunning, - merge( - lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, - glanceableHubToLockscreenTransitionViewModel.notificationAlpha, - ) - ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha -> - if (isOnGlanceableHubWithoutShade) { - // Notifications should not be visible on the glanceable hub. - // TODO(b/321075734): implement a way to actually set the notifications to gone - // while on the hub instead of just adjusting alpha - emit(0f) - } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) { - emit(alpha) - } else { - // Not on the hub and no transitions running, return full visibility so we don't - // block the notifications from showing. - emit(1f) + isOnGlanceableHubWithoutShade + .flatMapLatest { isOnGlanceableHubWithoutShade -> + combineTransform( + lockscreenToGlanceableHubRunning, + glanceableHubToLockscreenRunning, + merge( + lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, + glanceableHubToLockscreenTransitionViewModel.notificationAlpha, + ) + ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha -> + if (isOnGlanceableHubWithoutShade) { + // Notifications should not be visible on the glanceable hub. + // TODO(b/321075734): implement a way to actually set the notifications to + // gone + // while on the hub instead of just adjusting alpha + emit(0f) + } else if ( + lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning + ) { + emit(alpha) + } else { + // Not on the hub and no transitions running, return full visibility so we + // don't + // block the notifications from showing. + emit(1f) + } } } - } - .dumpWhileCollecting("glanceableHubAlpha") + .dumpWhileCollecting("glanceableHubAlpha") /** * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be @@ -464,20 +474,20 @@ constructor( */ fun translationY(params: BurnInParameters): Flow<Float> { return combine( - aodBurnInViewModel.translationY(params).onStart { emit(0f) }, - isOnLockscreenWithoutShade, - merge( - keyguardInteractor.keyguardTranslationY, - occludedToLockscreenTransitionViewModel.lockscreenTranslationY, - ) - ) { burnInY, isOnLockscreenWithoutShade, translationY -> - if (isOnLockscreenWithoutShade) { - burnInY + translationY - } else { - 0f + aodBurnInViewModel.translationY(params).onStart { emit(0f) }, + isOnLockscreenWithoutShade, + merge( + keyguardInteractor.keyguardTranslationY, + occludedToLockscreenTransitionViewModel.lockscreenTranslationY, + ) + ) { burnInY, isOnLockscreenWithoutShade, translationY -> + if (isOnLockscreenWithoutShade) { + burnInY + translationY + } else { + 0f + } } - } - .dumpWhileCollecting("translationY") + .dumpWhileCollecting("translationY") } /** @@ -486,10 +496,10 @@ constructor( */ val translationX: Flow<Float> = merge( - lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX, - glanceableHubToLockscreenTransitionViewModel.notificationTranslationX, - ) - .dumpWhileCollecting("translationX") + lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX, + glanceableHubToLockscreenTransitionViewModel.notificationTranslationX, + ) + .dumpWhileCollecting("translationX") /** * When on keyguard, there is limited space to display notifications so calculate how many could diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 560d5ba32fd6..48d3157b1968 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -315,8 +315,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mMetricsLogger.count("panel_open", 1); } else if (!mQsController.getExpanded() && !mShadeViewController.isExpandingOrCollapsing()) { - mQsController.flingQs(0 /* velocity */, - ShadeViewController.FLING_EXPAND); + mShadeController.animateExpandQs(); mMetricsLogger.count("panel_open_qs", 1); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index ee844345d2ec..ba89d4ac22cd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -95,6 +95,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewManagerInteractor; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.unfold.FoldAodAnimationController; @@ -351,8 +352,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private Lazy<WindowManagerLockscreenVisibilityInteractor> mWmLockscreenVisibilityInteractor; private Lazy<KeyguardSurfaceBehindInteractor> mSurfaceBehindInteractor; private Lazy<KeyguardDismissActionInteractor> mKeyguardDismissActionInteractor; - private final JavaAdapter mJavaAdapter; + private StatusBarKeyguardViewManagerInteractor mStatusBarKeyguardViewManagerInteractor; @Inject public StatusBarKeyguardViewManager( @@ -386,7 +387,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb SelectedUserInteractor selectedUserInteractor, Lazy<KeyguardSurfaceBehindInteractor> surfaceBehindInteractor, JavaAdapter javaAdapter, - Lazy<SceneInteractor> sceneInteractorLazy + Lazy<SceneInteractor> sceneInteractorLazy, + StatusBarKeyguardViewManagerInteractor statusBarKeyguardViewManagerInteractor ) { mContext = context; mViewMediatorCallback = callback; @@ -421,6 +423,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mSurfaceBehindInteractor = surfaceBehindInteractor; mJavaAdapter = javaAdapter; mSceneInteractorLazy = sceneInteractorLazy; + mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor; } KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -503,6 +506,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb lockscreenVis || animatingSurface ), this::consumeShowStatusBarKeyguardView); + + mJavaAdapter.alwaysCollectFlow( + mStatusBarKeyguardViewManagerInteractor.getKeyguardViewOcclusionState(), + (occlusionState) -> setOccluded( + occlusionState.getOccluded(), occlusionState.getAnimate())); } } @@ -1453,6 +1461,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb hideAlternateBouncer(false); executeAfterKeyguardGoneAction(); } + + if (KeyguardWmStateRefactor.isEnabled()) { + mKeyguardTransitionInteractor.startDismissKeyguardTransition(); + } } /** Display security message to relevant KeyguardMessageArea. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index f12a09b1062c..82d9fc7d0152 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -93,7 +93,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh /** * @deprecated Don't subclass SystemUIDialog. Please subclass {@link Delegate} and pass it to - * {@link Factory#create(DialogDelegate)} to create a custom dialog. + * {@link Factory#create(Delegate)} to create a custom dialog. */ @Deprecated public SystemUIDialog(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java index 0d6c0f59b2d0..10cf08221fb3 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java @@ -25,8 +25,11 @@ import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; +import com.android.app.tracing.TraceUtils; import com.android.systemui.settings.UserTracker; +import kotlin.Unit; + /** * Used to interact with per-user Settings.Secure and Settings.System settings (but not * Settings.Global, since those do not vary per-user) @@ -123,8 +126,16 @@ public interface UserSettingsProxy extends SettingsProxy { default void registerContentObserverForUser( Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver, int userHandle) { - getContentResolver().registerContentObserver( - uri, notifyForDescendants, settingsObserver, getRealUserHandle(userHandle)); + TraceUtils.trace( + () -> { + // The limit for trace tags length is 127 chars, which leaves us 90 for Uri. + return "USP#registerObserver#[" + uri.toString() + "]"; + }, () -> { + getContentResolver().registerContentObserver( + uri, notifyForDescendants, settingsObserver, + getRealUserHandle(userHandle)); + return Unit.INSTANCE; + }); } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 404563087041..23e296d70913 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -2082,6 +2082,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } else { row.anim.cancel(); row.anim.setIntValues(progress, newProgress); + // The animator can't keep up with the volume changes so haptics need to be + // triggered here. This happens when the volume keys are continuously pressed. + row.deliverOnProgressChangedHaptics(false, newProgress); } row.animTargetProgress = newProgress; row.anim.setDuration(UPDATE_ANIMATION_DURATION); @@ -2486,10 +2489,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (getActiveRow().equals(mRow) && mRow.slider.getVisibility() == VISIBLE && mRow.mHapticPlugin != null) { - mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser); - if (!fromUser) { - // Consider a change from program as the volume key being continuously pressed - mRow.mHapticPlugin.onKeyDown(); + if (fromUser || mRow.animTargetProgress == progress) { + // Deliver user-generated slider changes immediately, or when the animation + // completes + mRow.deliverOnProgressChangedHaptics(fromUser, progress); } } if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) @@ -2571,11 +2574,11 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, /* progressInterpolatorFactor= */ 1f, /* progressBasedDragMinScale= */ 0f, /* progressBasedDragMaxScale= */ 0.2f, - /* additionalVelocityMaxBump= */ 0.15f, + /* additionalVelocityMaxBump= */ 0.25f, /* deltaMillisForDragInterval= */ 0f, - /* deltaProgressForDragThreshold= */ 0.015f, - /* numberOfLowTicks= */ 5, - /* maxVelocityToScale= */ 300f, + /* deltaProgressForDragThreshold= */ 0.05f, + /* numberOfLowTicks= */ 4, + /* maxVelocityToScale= */ 200, /* velocityAxis= */ MotionEvent.AXIS_Y, /* upperBookendScale= */ 1f, /* lowerBookendScale= */ 0.05f, @@ -2642,6 +2645,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, void removeHaptics() { slider.setOnTouchListener(null); } + + void deliverOnProgressChangedHaptics(boolean fromUser, int progress) { + mHapticPlugin.onProgressChanged(slider, progress, fromUser); + if (!fromUser) { + // Consider a change from program as the volume key being continuously pressed + mHapticPlugin.onKeyDown(); + } + } } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java index 24a5e8072796..2e040077c227 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java @@ -15,14 +15,11 @@ package com.android.systemui; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; import android.os.Looper; import androidx.test.filters.SmallTest; -import com.android.systemui.statusbar.policy.FlashlightController; - import org.junit.Assert; import org.junit.Test; @@ -33,9 +30,9 @@ public class DependencyTest extends SysuiTestCase { @Test public void testClassDependency() { - FlashlightController f = mock(FlashlightController.class); - mDependency.injectTestDependency(FlashlightController.class, f); - Assert.assertEquals(f, Dependency.get(FlashlightController.class)); + FakeClass f = new FakeClass(); + mDependency.injectTestDependency(FakeClass.class, f); + Assert.assertEquals(f, Dependency.get(FakeClass.class)); } @Test @@ -53,4 +50,8 @@ public class DependencyTest extends SysuiTestCase { Dependency dependency = initializer.getSysUIComponent().createDependency(); dependency.start(); } + + private static class FakeClass { + + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java index 3da72618fb60..095c945ba77b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java @@ -22,10 +22,9 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGAT import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import android.app.ActivityManager; import android.content.Context; import android.content.ContextWrapper; import android.hardware.display.DisplayManager; @@ -75,13 +74,13 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { private AccessibilityManager mAccessibilityManager; private KeyguardUpdateMonitor mKeyguardUpdateMonitor; private AccessibilityFloatingMenuController mController; + @Mock private AccessibilityButtonTargetsObserver mTargetsObserver; + @Mock private AccessibilityButtonModeObserver mModeObserver; @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardCallbackCaptor; private KeyguardUpdateMonitorCallback mKeyguardCallback; - private int mLastButtonMode; - private String mLastButtonTargets; @Mock private SecureSettings mSecureSettings; @@ -97,10 +96,14 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { mWindowManager = mContext.getSystemService(WindowManager.class); mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); - mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT); - mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, UserHandle.USER_CURRENT); + + when(mTargetsObserver.getCurrentAccessibilityButtonTargets()) + .thenReturn(Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT)); + + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_MODE, UserHandle.USER_CURRENT)); } @After @@ -109,13 +112,6 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { mController.onAccessibilityButtonTargetsChanged(""); mController = null; } - - Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastButtonTargets, - UserHandle.USER_CURRENT); - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, mLastButtonMode, - UserHandle.USER_CURRENT); } @Test @@ -227,9 +223,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonModeChanged_floatingModeAndHasButtonTargets_showWidget() { - Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS, - ActivityManager.getCurrentUser()); + when(mTargetsObserver.getCurrentAccessibilityButtonTargets()) + .thenReturn(TEST_A11Y_BTN_TARGETS); mController = setUpController(); mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); @@ -239,8 +234,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonModeChanged_floatingModeAndNoButtonTargets_destroyWidget() { - Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser()); + when(mTargetsObserver.getCurrentAccessibilityButtonTargets()).thenReturn(""); + mController = setUpController(); mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); @@ -250,9 +245,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonModeChanged_navBarModeAndHasButtonTargets_destroyWidget() { - Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS, - ActivityManager.getCurrentUser()); + when(mTargetsObserver.getCurrentAccessibilityButtonTargets()) + .thenReturn(TEST_A11Y_BTN_TARGETS); mController = setUpController(); mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); @@ -262,8 +256,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonModeChanged_navBarModeAndNoButtonTargets_destroyWidget() { - Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", ActivityManager.getCurrentUser()); + when(mTargetsObserver.getCurrentAccessibilityButtonTargets()).thenReturn(""); mController = setUpController(); mController.onAccessibilityButtonModeChanged(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); @@ -273,9 +266,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonTargetsChanged_floatingModeAndHasButtonTargets_showWidget() { - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU, - ActivityManager.getCurrentUser()); + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); mController = setUpController(); mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS); @@ -285,9 +277,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonTargetsChanged_floatingModeAndNoButtonTargets_destroyWidget() { - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU, - ActivityManager.getCurrentUser()); + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); mController = setUpController(); mController.onAccessibilityButtonTargetsChanged(""); @@ -297,9 +288,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonTargetsChanged_navBarModeAndHasButtonTargets_destroyWidget() { - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, - ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser()); + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); mController = setUpController(); mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS); @@ -309,9 +299,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onAccessibilityButtonTargetsChanged_navBarModeAndNoButtonTargets_destroyWidget() { - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, - ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, ActivityManager.getCurrentUser()); + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); mController = setUpController(); mController.onAccessibilityButtonTargetsChanged(""); @@ -321,9 +310,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Test public void onTargetsChanged_isFloatingViewLayerControllerCreated() { - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU, - UserHandle.USER_CURRENT); + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); mController = setUpController(); mController.onAccessibilityButtonTargetsChanged(TEST_A11Y_BTN_TARGETS); @@ -335,8 +323,6 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); final FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext); - mTargetsObserver = spy(Dependency.get(AccessibilityButtonTargetsObserver.class)); - mModeObserver = spy(Dependency.get(AccessibilityButtonModeObserver.class)); mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class); final AccessibilityFloatingMenuController controller = new AccessibilityFloatingMenuController(mContextWrapper, windowManager, @@ -348,12 +334,11 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { } private void enableAccessibilityFloatingMenuConfig() { - Settings.Secure.putIntForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_MODE, ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU, - ActivityManager.getCurrentUser()); - Settings.Secure.putStringForUser(mContextWrapper.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, TEST_A11Y_BTN_TARGETS, - ActivityManager.getCurrentUser()); + when(mTargetsObserver.getCurrentAccessibilityButtonTargets()) + .thenReturn(TEST_A11Y_BTN_TARGETS); + + when(mModeObserver.getCurrentAccessibilityButtonMode()) + .thenReturn(ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); } private void captureKeyguardUpdateMonitorCallback() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index 915522de62d6..1a6da7608849 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -53,9 +53,11 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -101,6 +103,8 @@ class CustomizationProviderTest : SysuiTestCase() { private lateinit var underTest: CustomizationProvider private lateinit var testScope: TestScope + private val kosmos = testKosmos() + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -185,6 +189,7 @@ class CustomizationProviderTest : SysuiTestCase() { }, ) .keyguardInteractor, + shadeInteractor = kosmos.shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, userTracker = userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt index 489665cd130a..51828c91de4b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt @@ -6,6 +6,7 @@ import android.app.WindowConfiguration import android.graphics.Point import android.graphics.Rect import android.os.PowerManager +import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.RemoteAnimationTarget @@ -15,6 +16,7 @@ import android.view.View import android.view.ViewRootImpl import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController @@ -130,6 +132,7 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { * surface, or the user will see the wallpaper briefly as the app animates in. */ @Test + @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun noSurfaceAnimation_ifWakeAndUnlocking() { whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true) @@ -320,6 +323,7 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { * If we are not wake and unlocking, we expect the unlock animation to play normally. */ @Test + @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun surfaceAnimation_multipleTargets() { keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation( arrayOf(remoteTarget1, remoteTarget2), @@ -358,6 +362,7 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun surfaceBehindAlphaOverriddenTo0_ifNotInteractive() { whenever(powerManager.isInteractive).thenReturn(false) @@ -389,6 +394,7 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun surfaceBehindAlphaNotOverriddenTo0_ifInteractive() { whenever(powerManager.isInteractive).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 0957748c9938..0bd4cbec64dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -1263,7 +1263,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mSystemPropertiesHelper, () -> mock(WindowManagerLockscreenVisibilityManager.class), mSelectedUserInteractor, - mKeyguardInteractor); + mKeyguardInteractor, + mock(WindowManagerOcclusionManager.class)); mViewMediator.start(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt new file mode 100644 index 000000000000..7bef01a7a5ce --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.os.PowerManager +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.testKosmos +import junit.framework.Assert.assertEquals +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FromAodTransitionInteractorTest : SysuiTestCase() { + private val kosmos = + testKosmos().apply { + this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository()) + } + + private val testScope = kosmos.testScope + private val underTest = kosmos.fromAodTransitionInteractor + + private val powerInteractor = kosmos.powerInteractor + private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + @Before + fun setup() { + underTest.start() + + // Transition to AOD and set the power interactor asleep. + powerInteractor.setAsleepForTest() + runBlocking { + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope + ) + kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) + reset(transitionRepository) + } + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToLockscreen_onWakeup() = + testScope.runTest { + powerInteractor.setAwakeForTest() + runCurrent() + + // Under default conditions, we should transition to LOCKSCREEN when waking up. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() = + testScope.runTest { + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + powerInteractor.setAwakeForTest() + runCurrent() + + // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.AOD, + to = KeyguardState.OCCLUDED, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() = + testScope.runTest { + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + testScope, + ) + runCurrent() + + // Make sure we're GONE. + assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + + // Get part way to AOD. + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + testScope, + ) + runCurrent() + + // Make sure we're GONE. + assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + + // Get all the way to AOD + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + testScope = testScope, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should go to OCCLUDED - we came from GONE, but we finished in AOD, so this is no + // longer an insecure camera launch and it would be bad if we unlocked now. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + testScope, + ) + runCurrent() + + // Make sure we're in LOCKSCREEN. + assertEquals( + KeyguardState.LOCKSCREEN, + kosmos.keyguardTransitionInteractor.getFinishedState() + ) + + // Get part way to AOD. + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testWakeAndUnlock_transitionsToGone_onlyAfterDismissCallPostWakeup() = + testScope.runTest { + kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + powerInteractor.setAwakeForTest() + runCurrent() + + // Waking up from wake and unlock should not start any transitions, we'll wait for the + // dismiss call. + assertThat(transitionRepository).noTransitionsStarted() + + underTest.dismissAod() + runCurrent() + + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.AOD, to = KeyguardState.GONE) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt new file mode 100644 index 000000000000..258dbf3efbae --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.os.PowerManager +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.testKosmos +import junit.framework.Assert.assertEquals +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FromDozingTransitionInteractorTest : SysuiTestCase() { + private val kosmos = + testKosmos().apply { + this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository()) + } + + private val testScope = kosmos.testScope + private val underTest = kosmos.fromDozingTransitionInteractor + + private val powerInteractor = kosmos.powerInteractor + private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + @Before + fun setup() { + underTest.start() + + // Transition to DOZING and set the power interactor asleep. + powerInteractor.setAsleepForTest() + runBlocking { + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + testScope + ) + kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) + reset(transitionRepository) + } + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToLockscreen_onWakeup() = + testScope.runTest { + powerInteractor.setAwakeForTest() + runCurrent() + + // Under default conditions, we should transition to LOCKSCREEN when waking up. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.LOCKSCREEN, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() = + testScope.runTest { + kosmos.fakeCommunalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + runCurrent() + + powerInteractor.setAwakeForTest() + runCurrent() + + // Under default conditions, we should transition to LOCKSCREEN when waking up. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.GLANCEABLE_HUB, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() = + testScope.runTest { + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + powerInteractor.setAwakeForTest() + runCurrent() + + // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.OCCLUDED, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop_evenIfIdleOnCommunal() = + testScope.runTest { + kosmos.fakeCommunalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + runCurrent() + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + powerInteractor.setAwakeForTest() + runCurrent() + + // Waking with a SHOW_WHEN_LOCKED activity on top should transition to OCCLUDED. + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DOZING, + to = KeyguardState.OCCLUDED, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() = + testScope.runTest { + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.GONE, + testScope, + ) + runCurrent() + + // Make sure we're GONE. + assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + + // Get part way to AOD. + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.DOZING, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.GONE, + testScope, + ) + runCurrent() + + // Make sure we're GONE. + assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + + // Get all the way to AOD + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.DOZING, + testScope = testScope, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should go to OCCLUDED - we came from GONE, but we finished in AOD, so this is no + // longer an insecure camera launch and it would be bad if we unlocked now. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.DOZING, + to = KeyguardState.LOCKSCREEN, + testScope, + ) + runCurrent() + + // Make sure we're in LOCKSCREEN. + assertEquals( + KeyguardState.LOCKSCREEN, + kosmos.keyguardTransitionInteractor.getFinishedState() + ) + + // Get part way to AOD. + powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + // Detect a power gesture and then wake up. + reset(transitionRepository) + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + // We should head back to GONE since we started there. + assertThat(transitionRepository) + .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt new file mode 100644 index 000000000000..f534ba5bc68c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.testKosmos +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FromDreamingTransitionInteractorTest : SysuiTestCase() { + private val kosmos = + testKosmos().apply { + this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository()) + } + + private val testScope = kosmos.testScope + private val underTest = kosmos.fromDreamingTransitionInteractor + + private val powerInteractor = kosmos.powerInteractor + private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + @Before + fun setup() { + underTest.start() + + // Transition to DOZING and set the power interactor asleep. + powerInteractor.setAsleepForTest() + runBlocking { + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope + ) + kosmos.keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) + reset(transitionRepository) + } + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_ifDreamEnds_occludingActivityOnTop() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setDreaming(true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + runCurrent() + + reset(transitionRepository) + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true) + kosmos.fakeKeyguardRepository.setDreaming(false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DREAMING, + to = KeyguardState.OCCLUDED, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testDoesNotTransitionToOccluded_occludingActivityOnTop_whileStillDreaming() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setDreaming(true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + runCurrent() + + reset(transitionRepository) + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true) + runCurrent() + + assertThat(transitionRepository).noTransitionsStarted() + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionsToLockscreen_whenOccludingActivityEnds() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setDreaming(true) + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + runCurrent() + + reset(transitionRepository) + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.DREAMING, + to = KeyguardState.LOCKSCREEN, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt index 6aebe365dc8c..c3e24d579491 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.keyguard.domain.interactor +import android.app.ActivityManager +import android.app.WindowConfiguration import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,6 +28,7 @@ import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -42,6 +45,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never +import org.mockito.Mockito.reset import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -171,4 +175,49 @@ class FromLockscreenTransitionInteractorTest : SysuiTestCase() { assertThatRepository(transitionRepository).noTransitionsStarted() } + + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionsToOccluded_whenShowWhenLockedActivityOnTop() = + testScope.runTest { + underTest.start() + runCurrent() + + reset(transitionRepository) + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo( + true, + ActivityManager.RunningTaskInfo().apply { + topActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD + } + ) + runCurrent() + + assertThatRepository(transitionRepository) + .startedTransition( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionsToDream_whenDreamActivityOnTop() = + testScope.runTest { + underTest.start() + runCurrent() + + reset(transitionRepository) + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo( + true, + ActivityManager.RunningTaskInfo().apply { + topActivityType = WindowConfiguration.ACTIVITY_TYPE_DREAM + } + ) + runCurrent() + + assertThatRepository(transitionRepository) + .startedTransition( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt new file mode 100644 index 000000000000..d3c48483d100 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.testKosmos +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class FromOccludedTransitionInteractorTest : SysuiTestCase() { + private val kosmos = + testKosmos().apply { + this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository()) + } + + private val testScope = kosmos.testScope + private val underTest = kosmos.fromOccludedTransitionInteractor + + private val powerInteractor = kosmos.powerInteractor + private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + @Before + fun setup() { + underTest.start() + + // Transition to OCCLUDED and set up PowerInteractor and the occlusion repository. + powerInteractor.setAwakeForTest() + runBlocking { + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + testScope + ) + reset(transitionRepository) + } + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testShowWhenLockedActivity_noLongerOnTop_transitionsToLockscreen() = + testScope.runTest { + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testShowWhenLockedActivity_noLongerOnTop_transitionsToGlanceableHub_ifIdleOnCommunal() = + testScope.runTest { + kosmos.fakeCommunalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + runCurrent() + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.OCCLUDED, + to = KeyguardState.GLANCEABLE_HUB, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt index f33a5c9484ee..7ee8963aaa15 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt @@ -16,14 +16,23 @@ package com.android.systemui.keyguard.domain.interactor +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository 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.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.selectedUserInteractor @@ -31,20 +40,27 @@ import junit.framework.Assert.assertEquals import junit.framework.Assert.assertTrue import junit.framework.Assert.fail import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.reset +import org.mockito.Mockito.spy @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() { - val kosmos = testKosmos() + val kosmos = + testKosmos().apply { + this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository()) + } val underTest = kosmos.fromPrimaryBouncerTransitionInteractor val testScope = kosmos.testScope val selectedUserInteractor = kosmos.selectedUserInteractor val transitionRepository = kosmos.fakeKeyguardTransitionRepository + val bouncerRepository = kosmos.fakeKeyguardBouncerRepository @Test fun testSurfaceBehindVisibility() = @@ -193,4 +209,85 @@ class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() { fail("surfaceBehindModel was unexpectedly null.") } } + + @Test + @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testReturnToLockscreen_whenBouncerHides() = + testScope.runTest { + underTest.start() + bouncerRepository.setPrimaryShow(true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.PRIMARY_BOUNCER, + testScope + ) + + reset(transitionRepository) + + bouncerRepository.setPrimaryShow(false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.LOCKSCREEN + ) + } + + @Test + @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testReturnToGlanceableHub_whenBouncerHides_ifIdleOnCommunal() = + testScope.runTest { + underTest.start() + kosmos.fakeCommunalRepository.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal)) + ) + bouncerRepository.setPrimaryShow(true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.PRIMARY_BOUNCER, + testScope + ) + + reset(transitionRepository) + + bouncerRepository.setPrimaryShow(false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GLANCEABLE_HUB + ) + } + + @Test + @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testTransitionToOccluded_bouncerHide_occludingActivityOnTop() = + testScope.runTest { + underTest.start() + bouncerRepository.setPrimaryShow(true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.PRIMARY_BOUNCER, + testScope + ) + + reset(transitionRepository) + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + runCurrent() + + // Shouldn't transition to OCCLUDED until the bouncer hides. + assertThat(transitionRepository).noTransitionsStarted() + + bouncerRepository.setPrimaryShow(false) + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.OCCLUDED + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt new file mode 100644 index 000000000000..8a77ed2130a9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import kotlin.test.Test +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardOcclusionInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.keyguardOcclusionInteractor + private val powerInteractor = kosmos.powerInteractor + private val transitionRepository = kosmos.fakeKeyguardTransitionRepository + + @Test + fun testTransitionFromPowerGesture_whileGoingToSleep_isTrue() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING + ) + + powerInteractor.onCameraLaunchGestureDetected() + runCurrent() + + assertTrue(underTest.shouldTransitionFromPowerButtonGesture()) + } + + @Test + fun testTransitionFromPowerGesture_whileAsleep_isTrue() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + ) + + powerInteractor.onCameraLaunchGestureDetected() + runCurrent() + + assertTrue(underTest.shouldTransitionFromPowerButtonGesture()) + } + + @Test + fun testTransitionFromPowerGesture_whileWaking_isFalse() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + ) + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING + ) + + powerInteractor.onCameraLaunchGestureDetected() + runCurrent() + + assertFalse(underTest.shouldTransitionFromPowerButtonGesture()) + } + + @Test + fun testTransitionFromPowerGesture_whileAwake_isFalse() = + testScope.runTest { + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + ) + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + testScope = testScope, + ) + + powerInteractor.onCameraLaunchGestureDetected() + runCurrent() + + assertFalse(underTest.shouldTransitionFromPowerButtonGesture()) + } + + @Test + fun testShowWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() = + testScope.runTest { + val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture) + powerInteractor.setAsleepForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope = testScope, + ) + + powerInteractor.onCameraLaunchGestureDetected() + powerInteractor.setAwakeForTest() + runCurrent() + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + runCurrent() + + assertThat(values) + .containsExactly( + false, + true, + ) + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false) + runCurrent() + + assertThat(values) + .containsExactly( + false, + true, + false, + ) + + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + runCurrent() + + assertThat(values) + .containsExactly( + false, + true, + // Power button gesture was not triggered a second time, so this should remain + // false. + false, + ) + } + + @Test + fun testShowWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() = + testScope.runTest { + val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture) + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope = testScope, + ) + + powerInteractor.setAsleepForTest() + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING + ) + + powerInteractor.onCameraLaunchGestureDetected() + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true) + powerInteractor.setAwakeForTest() + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + testScope = testScope, + ) + + assertThat(values) + .containsExactly( + false, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index d2a8444e40ee..45b2a4266ff6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -46,7 +46,9 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -242,6 +244,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository private lateinit var userTracker: UserTracker + private val kosmos = testKosmos() + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -311,6 +315,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { featureFlags = featureFlags, ) .keyguardInteractor, + shadeInteractor = kosmos.shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, userTracker = userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index c65a9ef28bba..95606ae81e5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -21,8 +21,8 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN +import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.domain.interactor.communalInteractor @@ -40,7 +40,6 @@ import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -48,7 +47,6 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.statusbar.commandQueue import com.android.systemui.testKosmos -import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -92,30 +90,26 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { private var commandQueue = kosmos.fakeCommandQueue private val shadeRepository = kosmos.fakeShadeRepository private val transitionRepository = kosmos.fakeKeyguardTransitionRepository - private val transitionInteractor = kosmos.keyguardTransitionInteractor private lateinit var featureFlags: FakeFeatureFlags // Used to verify transition requests for test output @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel - @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor private val fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor - private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor - private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor - private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor - private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor - private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor - private lateinit var fromAlternateBouncerTransitionInteractor: - FromAlternateBouncerTransitionInteractor + private val fromDreamingTransitionInteractor = kosmos.fromDreamingTransitionInteractor + private val fromDozingTransitionInteractor = kosmos.fromDozingTransitionInteractor + private val fromOccludedTransitionInteractor = kosmos.fromOccludedTransitionInteractor + private val fromGoneTransitionInteractor = kosmos.fromGoneTransitionInteractor + private val fromAodTransitionInteractor = kosmos.fromAodTransitionInteractor + private val fromAlternateBouncerTransitionInteractor = + kosmos.fromAlternateBouncerTransitionInteractor private val fromPrimaryBouncerTransitionInteractor = kosmos.fromPrimaryBouncerTransitionInteractor - private lateinit var fromDreamingLockscreenHostedTransitionInteractor: - FromDreamingLockscreenHostedTransitionInteractor - private lateinit var fromGlanceableHubTransitionInteractor: - FromGlanceableHubTransitionInteractor + private val fromDreamingLockscreenHostedTransitionInteractor = + kosmos.fromDreamingLockscreenHostedTransitionInteractor + private val fromGlanceableHubTransitionInteractor = kosmos.fromGlanceableHubTransitionInteractor private val powerInteractor = kosmos.powerInteractor - private val keyguardInteractor = kosmos.keyguardInteractor private val communalInteractor = kosmos.communalInteractor @Before @@ -125,122 +119,21 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + mSetFlagsRule.disableFlags( + Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) featureFlags = FakeFeatureFlags() - val glanceableHubTransitions = - GlanceableHubTransitions( - bgDispatcher = kosmos.testDispatcher, - transitionInteractor = transitionInteractor, - transitionRepository = transitionRepository, - communalInteractor = communalInteractor - ) - fromLockscreenTransitionInteractor.start() fromPrimaryBouncerTransitionInteractor.start() - - fromDreamingTransitionInteractor = - FromDreamingTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - glanceableHubTransitions = glanceableHubTransitions, - ) - .apply { start() } - - fromDreamingLockscreenHostedTransitionInteractor = - FromDreamingLockscreenHostedTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - ) - .apply { start() } - - fromAodTransitionInteractor = - FromAodTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - powerInteractor = powerInteractor, - ) - .apply { start() } - - fromGoneTransitionInteractor = - FromGoneTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - powerInteractor = powerInteractor, - communalInteractor = communalInteractor, - ) - .apply { start() } - - fromDozingTransitionInteractor = - FromDozingTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - powerInteractor = powerInteractor, - communalInteractor = communalInteractor, - ) - .apply { start() } - - fromOccludedTransitionInteractor = - FromOccludedTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - powerInteractor = powerInteractor, - communalInteractor = communalInteractor, - ) - .apply { start() } - - fromAlternateBouncerTransitionInteractor = - FromAlternateBouncerTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - communalInteractor = communalInteractor, - powerInteractor = powerInteractor, - ) - .apply { start() } - - fromGlanceableHubTransitionInteractor = - FromGlanceableHubTransitionInteractor( - scope = testScope, - bgDispatcher = kosmos.testDispatcher, - mainDispatcher = kosmos.testDispatcher, - glanceableHubTransitions = glanceableHubTransitions, - keyguardInteractor = keyguardInteractor, - transitionRepository = transitionRepository, - transitionInteractor = transitionInteractor, - powerInteractor = powerInteractor, - ) - .apply { start() } - - mSetFlagsRule.disableFlags( - FLAG_KEYGUARD_WM_STATE_REFACTOR, - ) + fromDreamingTransitionInteractor.start() + fromDreamingLockscreenHostedTransitionInteractor.start() + fromAodTransitionInteractor.start() + fromGoneTransitionInteractor.start() + fromDozingTransitionInteractor.start() + fromOccludedTransitionInteractor.start() + fromAlternateBouncerTransitionInteractor.start() + fromGlanceableHubTransitionInteractor.start() } @Test @@ -257,7 +150,9 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.PRIMARY_BOUNCER, from = KeyguardState.LOCKSCREEN, - ownerName = "FromLockscreenTransitionInteractor", + ownerName = + "FromLockscreenTransitionInteractor" + + "(#listenForLockscreenToPrimaryBouncer)", animatorAssertion = { it.isNotNull() } ) @@ -282,7 +177,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.DOZING, from = KeyguardState.OCCLUDED, - ownerName = "FromOccludedTransitionInteractor", + ownerName = "FromOccludedTransitionInteractor(Sleep transition triggered)", animatorAssertion = { it.isNotNull() } ) @@ -307,7 +202,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.AOD, from = KeyguardState.OCCLUDED, - ownerName = "FromOccludedTransitionInteractor", + ownerName = "FromOccludedTransitionInteractor(Sleep transition triggered)", animatorAssertion = { it.isNotNull() } ) @@ -389,7 +284,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.DOZING, from = KeyguardState.LOCKSCREEN, - ownerName = "FromLockscreenTransitionInteractor", + ownerName = "FromLockscreenTransitionInteractor(Sleep transition triggered)", animatorAssertion = { it.isNotNull() } ) @@ -414,7 +309,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.AOD, from = KeyguardState.LOCKSCREEN, - ownerName = "FromLockscreenTransitionInteractor", + ownerName = "FromLockscreenTransitionInteractor(Sleep transition triggered)", animatorAssertion = { it.isNotNull() } ) @@ -703,6 +598,32 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + /** This handles security method NONE and screen off with lock timeout */ + @Test + fun dreamingToGoneWithKeyguardNotShowing() = + testScope.runTest { + // GIVEN a prior transition has run to DREAMING + keyguardRepository.setDreamingWithOverlay(true) + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING) + runCurrent() + + // WHEN the device wakes up without a keyguard + keyguardRepository.setKeyguardShowing(false) + keyguardRepository.setKeyguardDismissible(true) + keyguardRepository.setDreamingWithOverlay(false) + advanceTimeBy(60L) + + assertThat(transitionRepository) + .startedTransition( + to = KeyguardState.GONE, + from = KeyguardState.DREAMING, + ownerName = "FromDreamingTransitionInteractor", + animatorAssertion = { it.isNotNull() } + ) + + coroutineContext.cancelChildren() + } + @Test fun dozingToGlanceableHub() = testScope.runTest { @@ -752,7 +673,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.DOZING, from = KeyguardState.GONE, - ownerName = "FromGoneTransitionInteractor", + ownerName = "FromGoneTransitionInteractor(Sleep transition triggered)", animatorAssertion = { it.isNotNull() } ) @@ -777,7 +698,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { .startedTransition( to = KeyguardState.AOD, from = KeyguardState.GONE, - ownerName = "FromGoneTransitionInteractor", + ownerName = "FromGoneTransitionInteractor(Sleep transition triggered)", animatorAssertion = { it.isNotNull() } ) @@ -1044,12 +965,14 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { @Test fun primaryBouncerToAod() = testScope.runTest { + // GIVEN aod available + keyguardRepository.setAodAvailable(true) + runCurrent() + // GIVEN a prior transition has run to PRIMARY_BOUNCER bouncerRepository.setPrimaryShow(true) runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER) - // GIVEN aod available and starting to sleep - keyguardRepository.setAodAvailable(true) powerInteractor.setAsleepForTest() // WHEN the primaryBouncer stops showing @@ -1059,7 +982,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // THEN a transition to AOD should occur assertThat(transitionRepository) .startedTransition( - ownerName = "FromPrimaryBouncerTransitionInteractor", + ownerName = + "FromPrimaryBouncerTransitionInteractor" + "(Sleep transition triggered)", from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.AOD, animatorAssertion = { it.isNotNull() }, @@ -1086,7 +1010,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // THEN a transition to DOZING should occur assertThat(transitionRepository) .startedTransition( - ownerName = "FromPrimaryBouncerTransitionInteractor", + ownerName = + "FromPrimaryBouncerTransitionInteractor" + "(Sleep transition triggered)", from = KeyguardState.PRIMARY_BOUNCER, to = KeyguardState.DOZING, animatorAssertion = { it.isNotNull() }, @@ -1616,7 +1541,9 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur assertThat(transitionRepository) .startedTransition( - ownerName = "FromLockscreenTransitionInteractor", + ownerName = + "FromLockscreenTransitionInteractor" + + "(#listenForLockscreenToPrimaryBouncerDragging)", from = KeyguardState.LOCKSCREEN, to = KeyguardState.PRIMARY_BOUNCER, animatorAssertion = { it.isNull() }, // dragging should be manually animated diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 2ec2fe3d0eb7..729086388a4f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -54,6 +54,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import com.android.systemui.statusbar.policy.KeyguardStateController @@ -218,6 +219,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { quickAffordanceInteractor = KeyguardQuickAffordanceInteractor( keyguardInteractor = keyguardInteractor, + shadeInteractor = kosmos.shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, userTracker = userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 1f14afa1d00b..bcec6109faf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -280,6 +280,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { quickAffordanceInteractor = KeyguardQuickAffordanceInteractor( keyguardInteractor = keyguardInteractor, + shadeInteractor = shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, userTracker = userTracker, @@ -643,7 +644,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { val testConfig = TestConfig( - isVisible = true, + isVisible = false, isClickable = false, icon = mock(), canShowWhileLocked = false, @@ -673,7 +674,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { val testConfig = TestConfig( - isVisible = true, + isVisible = false, isClickable = false, icon = mock(), canShowWhileLocked = false, diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt index dbfab64b004a..bda0e1ed5c46 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt @@ -21,32 +21,25 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken +import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken +import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository +import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @SmallTest class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() { - private val fakeActivityTaskManager = FakeActivityTaskManager() - - private val dispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(dispatcher) - - private val repo = - ActivityTaskManagerTasksRepository( - activityTaskManager = fakeActivityTaskManager.activityTaskManager, - applicationScope = testScope.backgroundScope, - backgroundDispatcher = dispatcher - ) + private val kosmos = taskSwitcherKosmos() + private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager + private val testScope = kosmos.testScope + private val repo = kosmos.activityTaskManagerTasksRepository @Test fun launchRecentTask_taskIsMovedToForeground() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt index fdd434acdc9f..6043ede66b31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -17,50 +17,35 @@ package com.android.systemui.mediaprojection.taskswitcher.data.repository import android.os.Binder -import android.os.Handler import android.testing.AndroidTestingRunner import android.view.ContentRecordingSession import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createToken +import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.mediaProjectionManagerRepository +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @SmallTest class MediaProjectionManagerRepositoryTest : SysuiTestCase() { - private val dispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(dispatcher) - - private val fakeMediaProjectionManager = FakeMediaProjectionManager() - private val fakeActivityTaskManager = FakeActivityTaskManager() - - private val tasksRepo = - ActivityTaskManagerTasksRepository( - activityTaskManager = fakeActivityTaskManager.activityTaskManager, - applicationScope = testScope.backgroundScope, - backgroundDispatcher = dispatcher - ) - - private val repo = - MediaProjectionManagerRepository( - mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, - handler = Handler.getMain(), - applicationScope = testScope.backgroundScope, - tasksRepository = tasksRepo, - backgroundDispatcher = dispatcher, - mediaProjectionServiceHelper = fakeMediaProjectionManager.helper - ) + private val kosmos = taskSwitcherKosmos() + private val testScope = kosmos.testScope + + private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager + private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager + + private val repo = kosmos.mediaProjectionManagerRepository @Test fun switchProjectedTask_stateIsUpdatedWithNewTask() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt index dfb688bbde4b..33e65f26716a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt @@ -17,55 +17,33 @@ package com.android.systemui.mediaprojection.taskswitcher.domain.interactor import android.content.Intent -import android.os.Handler import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession -import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createSingleTaskSession import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState +import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherInteractor +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @SmallTest class TaskSwitchInteractorTest : SysuiTestCase() { - private val dispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(dispatcher) - - private val fakeActivityTaskManager = FakeActivityTaskManager() - private val fakeMediaProjectionManager = FakeMediaProjectionManager() - - private val tasksRepo = - ActivityTaskManagerTasksRepository( - activityTaskManager = fakeActivityTaskManager.activityTaskManager, - applicationScope = testScope.backgroundScope, - backgroundDispatcher = dispatcher - ) - - private val mediaRepo = - MediaProjectionManagerRepository( - mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, - handler = Handler.getMain(), - applicationScope = testScope.backgroundScope, - tasksRepository = tasksRepo, - backgroundDispatcher = dispatcher, - mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, - ) - - private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) + private val kosmos = taskSwitcherKosmos() + private val testScope = kosmos.testScope + private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager + private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager + private val interactor = kosmos.taskSwitcherInteractor @Test fun taskSwitchChanges_notProjecting_foregroundTaskChange_emitsNotProjectingTask() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt index c4e939339fa1..9382c5882b25 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt @@ -18,26 +18,22 @@ package com.android.systemui.mediaprojection.taskswitcher.ui import android.app.Notification import android.app.NotificationManager -import android.os.Handler import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager -import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository -import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor -import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel +import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherViewModel import com.android.systemui.res.R import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -46,39 +42,16 @@ import org.mockito.ArgumentCaptor import org.mockito.Mockito.never import org.mockito.Mockito.verify -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @SmallTest class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() { private val notificationManager = mock<NotificationManager>() - - private val dispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(dispatcher) - - private val fakeActivityTaskManager = FakeActivityTaskManager() - private val fakeMediaProjectionManager = FakeMediaProjectionManager() - - private val tasksRepo = - ActivityTaskManagerTasksRepository( - activityTaskManager = fakeActivityTaskManager.activityTaskManager, - applicationScope = testScope.backgroundScope, - backgroundDispatcher = dispatcher - ) - - private val mediaRepo = - MediaProjectionManagerRepository( - mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, - handler = Handler.getMain(), - applicationScope = testScope.backgroundScope, - tasksRepository = tasksRepo, - backgroundDispatcher = dispatcher, - mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, - ) - - private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) - private val viewModel = - TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher) + private val kosmos = taskSwitcherKosmos() + private val testScope = kosmos.testScope + private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager + private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager + private val viewModel = kosmos.taskSwitcherViewModel private lateinit var coordinator: TaskSwitcherNotificationCoordinator diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt index 5dadf21a46b9..a468953b971e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt @@ -17,60 +17,35 @@ package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel import android.content.Intent -import android.os.Handler import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager.Companion.createTask -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createDisplaySession -import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionManager.Companion.createSingleTaskSession -import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository -import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession +import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createSingleTaskSession +import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager +import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos +import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherViewModel import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState +import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel.Companion.NOTIFICATION_MAX_SHOW_DURATION import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @SmallTest class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { - private val dispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(dispatcher) - - private val fakeActivityTaskManager = FakeActivityTaskManager() - private val fakeMediaProjectionManager = FakeMediaProjectionManager() - - private val tasksRepo = - ActivityTaskManagerTasksRepository( - activityTaskManager = fakeActivityTaskManager.activityTaskManager, - applicationScope = testScope.backgroundScope, - backgroundDispatcher = dispatcher - ) - - private val mediaRepo = - MediaProjectionManagerRepository( - mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, - handler = Handler.getMain(), - applicationScope = testScope.backgroundScope, - tasksRepository = tasksRepo, - backgroundDispatcher = dispatcher, - mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, - ) - - private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo) - - private val viewModel = - TaskSwitcherNotificationViewModel(interactor, backgroundDispatcher = dispatcher) + private val kosmos = taskSwitcherKosmos() + private val testScope = kosmos.testScope + private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager + private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager + private val viewModel = kosmos.taskSwitcherViewModel @Test fun uiState_notProjecting_emitsNotShowing() = @@ -138,6 +113,41 @@ class TaskSwitcherNotificationViewModelTest : SysuiTestCase() { } @Test + fun uiState_taskChanged_beforeDelayLimit_stillEmitsShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + testScheduler.advanceTimeBy(NOTIFICATION_MAX_SHOW_DURATION - 1.milliseconds) + assertThat(uiState) + .isEqualTo(TaskSwitcherNotificationUiState.Showing(projectedTask, foregroundTask)) + } + + @Test + fun uiState_taskChanged_afterDelayLimit_emitsNotShowing() = + testScope.runTest { + val projectedTask = createTask(taskId = 1) + val foregroundTask = createTask(taskId = 2) + val uiState by collectLastValue(viewModel.uiState) + + fakeActivityTaskManager.addRunningTasks(projectedTask, foregroundTask) + fakeMediaProjectionManager.dispatchOnSessionSet( + session = createSingleTaskSession(projectedTask.token.asBinder()) + ) + fakeActivityTaskManager.moveTaskToForeground(foregroundTask) + + testScheduler.advanceTimeBy(NOTIFICATION_MAX_SHOW_DURATION) + assertThat(uiState).isEqualTo(TaskSwitcherNotificationUiState.NotShowing) + } + + @Test fun uiState_projectingTask_foregroundTaskChanged_thenTaskSwitched_emitsNotShowing() = testScope.runTest { val projectedTask = createTask(taskId = 1) diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java index 52859cdeb406..d405df7c2cba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java @@ -43,13 +43,11 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper.RunWithLooper; import android.util.SparseArray; import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.StaticMockitoSession; -import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.model.SysUiState; @@ -61,7 +59,9 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.FakeSystemClock; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.pip.Pip; @@ -76,7 +76,6 @@ import java.util.Optional; /** atest NavigationBarControllerTest */ @RunWith(AndroidTestingRunner.class) -@RunWithLooper @SmallTest public class NavigationBarControllerImplTest extends SysuiTestCase { @@ -88,6 +87,8 @@ public class NavigationBarControllerImplTest extends SysuiTestCase { private StaticMockitoSession mMockitoSession; private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); + private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); + @Mock private CommandQueue mCommandQueue; @Mock @@ -104,7 +105,7 @@ public class NavigationBarControllerImplTest extends SysuiTestCase { mock(NavigationModeController.class), mock(SysUiState.class), mCommandQueue, - Dependency.get(Dependency.MAIN_HANDLER), + mExecutor, mock(ConfigurationController.class), mock(NavBarHelper.class), mTaskbarDelegate, diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index 7b285abe83ce..ada93db537e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.SessionCreationSource import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver +import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate import com.android.systemui.model.SysUiState import com.android.systemui.qs.tiles.RecordIssueTile import com.android.systemui.res.R @@ -41,18 +42,18 @@ import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -74,12 +75,16 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var userFileManager: UserFileManager @Mock private lateinit var sharedPreferences: SharedPreferences + @Mock private lateinit var screenCaptureDisabledDialogDelegate: + ScreenCaptureDisabledDialogDelegate + @Mock private lateinit var screenCaptureDisabledDialog: SystemUIDialog @Mock private lateinit var sysuiState: SysUiState @Mock private lateinit var systemUIDialogManager: SystemUIDialogManager @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var bgExecutor: Executor - @Mock private lateinit var mainExecutor: Executor + private val systemClock = FakeSystemClock() + private val bgExecutor = FakeExecutor(systemClock) + private val mainExecutor = FakeExecutor(systemClock) @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator private lateinit var dialog: SystemUIDialog @@ -92,6 +97,8 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { whenever(dprLazy.get()).thenReturn(devicePolicyResolver) whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState) whenever(userContextProvider.userContext).thenReturn(mContext) + whenever(screenCaptureDisabledDialogDelegate.createDialog()) + .thenReturn(screenCaptureDisabledDialog) whenever( userFileManager.getSharedPreferences( eq(RecordIssueTile.TILE_SPEC), @@ -124,6 +131,7 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { dprLazy, mediaProjectionMetricsLogger, userFileManager, + screenCaptureDisabledDialogDelegate, ) { latch.countDown() } @@ -163,13 +171,8 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch) screenRecordSwitch.isChecked = true - val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(bgExecutor).execute(bgCaptor.capture()) - bgCaptor.value.run() - - val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(mainExecutor).execute(mainCaptor.capture()) - mainCaptor.value.run() + bgExecutor.runAllReady() + mainExecutor.runAllReady() verify(mediaProjectionMetricsLogger, never()) .notifyProjectionInitiated( @@ -192,13 +195,8 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch) screenRecordSwitch.isChecked = true - val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(bgExecutor).execute(bgCaptor.capture()) - bgCaptor.value.run() - - val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(mainExecutor).execute(mainCaptor.capture()) - mainCaptor.value.run() + bgExecutor.runAllReady() + mainExecutor.runAllReady() verify(mediaProjectionMetricsLogger) .notifyProjectionInitiated( @@ -219,9 +217,7 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch) screenRecordSwitch.isChecked = true - val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(bgExecutor).execute(bgCaptor.capture()) - bgCaptor.value.run() + bgExecutor.runAllReady() verify(mediaProjectionMetricsLogger) .notifyProjectionInitiated( diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index 6cbe8c9a939b..b3df12ee2c10 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -17,13 +17,11 @@ package com.android.systemui.screenrecord; import static android.os.Process.myUid; - import static com.google.common.truth.Truth.assertThat; - import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; - import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -48,10 +46,9 @@ import com.android.systemui.flags.Flags; import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.mediaprojection.SessionCreationSource; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; -import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; +import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.DialogDelegate; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -84,8 +81,6 @@ public class RecordingControllerTest extends SysuiTestCase { @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock - private UserContextProvider mUserContextProvider; - @Mock private ScreenCaptureDevicePolicyResolver mDevicePolicyResolver; @Mock private DialogTransitionAnimator mDialogTransitionAnimator; @@ -96,6 +91,22 @@ public class RecordingControllerTest extends SysuiTestCase { @Mock private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; + @Mock + private ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate; + @Mock + private SystemUIDialog mScreenCaptureDisabledDialog; + @Mock + private ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory; + @Mock + private ScreenRecordDialogDelegate mScreenRecordDialogDelegate; + @Mock + private ScreenRecordPermissionDialogDelegate.Factory + mScreenRecordPermissionDialogDelegateFactory; + @Mock + private ScreenRecordPermissionDialogDelegate mScreenRecordPermissionDialogDelegate; + @Mock + private SystemUIDialog mScreenRecordSystemUIDialog; + private FakeFeatureFlags mFeatureFlags; private RecordingController mController; private TestSystemUIDialogFactory mDialogFactory; @@ -108,8 +119,6 @@ public class RecordingControllerTest extends SysuiTestCase { Context spiedContext = spy(mContext); when(spiedContext.getUserId()).thenReturn(TEST_USER_ID); - when(mUserContextProvider.getUserContext()).thenReturn(spiedContext); - mDialogFactory = new TestSystemUIDialogFactory( mContext, Dependency.get(SystemUIDialogManager.class), @@ -119,16 +128,26 @@ public class RecordingControllerTest extends SysuiTestCase { ); mFeatureFlags = new FakeFeatureFlags(); + when(mScreenCaptureDisabledDialogDelegate.createDialog()) + .thenReturn(mScreenCaptureDisabledDialog); + when(mScreenRecordDialogFactory.create(any(), any())) + .thenReturn(mScreenRecordDialogDelegate); + when(mScreenRecordDialogDelegate.createDialog()).thenReturn(mScreenRecordSystemUIDialog); + when(mScreenRecordPermissionDialogDelegateFactory.create(any(), any(), anyInt(), any())) + .thenReturn(mScreenRecordPermissionDialogDelegate); + when(mScreenRecordPermissionDialogDelegate.createDialog()) + .thenReturn(mScreenRecordSystemUIDialog); mController = new RecordingController( mMainExecutor, mBroadcastDispatcher, - mContext, mFeatureFlags, - mUserContextProvider, () -> mDevicePolicyResolver, mUserTracker, mMediaProjectionMetricsLogger, - mDialogFactory); + mScreenCaptureDisabledDialogDelegate, + mScreenRecordDialogFactory, + mScreenRecordPermissionDialogDelegateFactory + ); mController.addCallback(mCallback); } @@ -242,8 +261,8 @@ public class RecordingControllerTest extends SysuiTestCase { mActivityStarter, /* onStartRecordingClicked= */ null); - assertThat(dialog).isSameInstanceAs(mDialogFactory.mLastCreatedDialog); - assertThat(mDialogFactory.mLastDelegate) + assertThat(dialog).isSameInstanceAs(mScreenRecordSystemUIDialog); + assertThat(mScreenRecordPermissionDialogDelegate) .isInstanceOf(ScreenRecordPermissionDialogDelegate.class); } @@ -255,7 +274,7 @@ public class RecordingControllerTest extends SysuiTestCase { Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags, mDialogTransitionAnimator, mActivityStarter, /* onStartRecordingClicked= */ null); - assertThat(dialog).isInstanceOf(ScreenRecordDialog.class); + assertThat(dialog).isEqualTo(mScreenRecordSystemUIDialog); } @Test @@ -267,7 +286,7 @@ public class RecordingControllerTest extends SysuiTestCase { Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags, mDialogTransitionAnimator, mActivityStarter, /* onStartRecordingClicked= */ null); - assertThat(dialog).isInstanceOf(ScreenCaptureDisabledDialog.class); + assertThat(dialog).isEqualTo(mScreenCaptureDisabledDialog); } @Test @@ -284,8 +303,8 @@ public class RecordingControllerTest extends SysuiTestCase { mActivityStarter, /* onStartRecordingClicked= */ null); - assertThat(dialog).isSameInstanceAs(mDialogFactory.mLastCreatedDialog); - assertThat(mDialogFactory.mLastDelegate) + assertThat(dialog).isSameInstanceAs(mScreenRecordSystemUIDialog); + assertThat(mScreenRecordPermissionDialogDelegate) .isInstanceOf(ScreenRecordPermissionDialogDelegate.class); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt index 90ced92c7f30..6e480746a076 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.res.R import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat @@ -58,6 +59,7 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { + //@Mock private lateinit var dialogFactory: SystemUIDialog.Factory @Mock private lateinit var starter: ActivityStarter @Mock private lateinit var controller: RecordingController @Mock private lateinit var userContextProvider: UserContextProvider @@ -71,14 +73,17 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true) + val systemUIDialogFactory = - SystemUIDialog.Factory( - context, - Dependency.get(SystemUIDialogManager::class.java), - Dependency.get(SysUiState::class.java), - Dependency.get(BroadcastDispatcher::class.java), - Dependency.get(DialogTransitionAnimator::class.java), - ) + SystemUIDialog.Factory( + context, + Dependency.get(SystemUIDialogManager::class.java), + Dependency.get(SysUiState::class.java), + Dependency.get(BroadcastDispatcher::class.java), + Dependency.get(DialogTransitionAnimator::class.java), + ) + val delegate = ScreenRecordPermissionDialogDelegate( UserHandle.of(0), @@ -88,11 +93,9 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { userContextProvider, onStartRecordingClicked, mediaProjectionMetricsLogger, - systemUIDialogFactory + systemUIDialogFactory, ) dialog = delegate.createDialog() - delegate.onCreate(dialog, savedInstanceState = null) - whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true) } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt index 2f911fffe335..92c240404b24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt @@ -22,8 +22,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import java.lang.IllegalStateException +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -31,12 +33,14 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify @SmallTest +@OptIn(ExperimentalCoroutinesApi::class) class ScreenshotSoundControllerTest : SysuiTestCase() { private val soundProvider = mock<ScreenshotSoundProvider>() private val mediaPlayer = mock<MediaPlayer>() private val bgDispatcher = UnconfinedTestDispatcher() private val scope = TestScope(bgDispatcher) + @Before fun setup() { whenever(soundProvider.getScreenshotSound()).thenReturn(mediaPlayer) @@ -45,52 +49,59 @@ class ScreenshotSoundControllerTest : SysuiTestCase() { @Test fun init_soundLoading() { createController() - bgDispatcher.scheduler.runCurrent() + scope.advanceUntilIdle() verify(soundProvider).getScreenshotSound() } @Test - fun init_soundLoadingException_playAndReleaseDoNotThrow() = runTest { - whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException()) + fun init_soundLoadingException_playAndReleaseDoNotThrow() = + scope.runTest { + whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException()) - val controller = createController() + val controller = createController() - controller.playCameraSound().await() - controller.releaseScreenshotSound().await() + controller.playScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer, never()).start() - verify(mediaPlayer, never()).release() - } + verify(mediaPlayer, never()).start() + verify(mediaPlayer, never()).release() + } @Test - fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() = runTest { - val controller = createController() + fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() = + scope.runTest { + val controller = createController() - controller.playCameraSound().await() + controller.playScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer).start() - } + verify(mediaPlayer).start() + } @Test - fun playCameraSound_illegalStateException_doesNotThrow() = runTest { - whenever(mediaPlayer.start()).thenThrow(IllegalStateException()) + fun playCameraSound_illegalStateException_doesNotThrow() = + scope.runTest { + whenever(mediaPlayer.start()).thenThrow(IllegalStateException()) - val controller = createController() - controller.playCameraSound().await() + val controller = createController() + controller.playScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer).start() - verify(mediaPlayer).release() - } + verify(mediaPlayer).start() + verify(mediaPlayer).release() + } @Test - fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() = runTest { - val controller = createController() + fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() = + scope.runTest { + val controller = createController() - controller.releaseScreenshotSound().await() + controller.releaseScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer).release() - } + verify(mediaPlayer).release() + } private fun createController() = ScreenshotSoundControllerImpl(soundProvider, scope, bgDispatcher) 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 fd7b1399d03f..acbf9976873a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -287,7 +287,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected KeyguardMediaController mKeyguardMediaController; @Mock protected NavigationModeController mNavigationModeController; @Mock protected NavigationBarController mNavigationBarController; - @Mock protected QuickSettingsController mQsController; + @Mock protected QuickSettingsControllerImpl mQsController; @Mock protected ShadeHeaderController mShadeHeaderController; @Mock protected ContentResolver mContentResolver; @Mock protected TapAgainViewController mTapAgainViewController; @@ -380,7 +380,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected final ShadeExpansionStateManager mShadeExpansionStateManager = new ShadeExpansionStateManager(); - protected QuickSettingsController mQuickSettingsController; + protected QuickSettingsControllerImpl mQuickSettingsController; @Mock protected Lazy<NotificationPanelViewController> mNotificationPanelViewControllerLazy; protected FragmentHostManager.FragmentListener mFragmentListener; @@ -794,7 +794,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mNotificationPanelViewControllerLazy.get()) .thenReturn(mNotificationPanelViewController); - mQuickSettingsController = new QuickSettingsController( + mQuickSettingsController = new QuickSettingsControllerImpl( mNotificationPanelViewControllerLazy, mView, mQsFrameTranslateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 960fd59b4f10..617b25d97eee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -112,7 +112,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController - @Mock private lateinit var quickSettingsController: QuickSettingsController + @Mock private lateinit var quickSettingsController: QuickSettingsControllerImpl @Mock private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController @Mock private lateinit var lockIconViewController: LockIconViewController diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 0b49a954e848..4809a47709c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -109,7 +109,7 @@ import org.mockito.MockitoAnnotations; import kotlinx.coroutines.test.TestScope; -public class QuickSettingsControllerBaseTest extends SysuiTestCase { +public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { protected static final float QS_FRAME_START_X = 0f; protected static final int QS_FRAME_WIDTH = 1000; protected static final int QS_FRAME_TOP = 0; @@ -119,7 +119,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { protected static final int DEFAULT_MIN_HEIGHT_SPLIT_SHADE = DEFAULT_HEIGHT; protected static final int DEFAULT_MIN_HEIGHT = 300; - protected QuickSettingsController mQsController; + protected QuickSettingsControllerImpl mQsController; protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); protected TestScope mTestScope = mKosmos.getTestScope(); @@ -304,7 +304,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mMainHandler = new Handler(Looper.getMainLooper()); - mQsController = new QuickSettingsController( + mQsController = new QuickSettingsControllerImpl( mPanelViewControllerLazy, mPanelView, mQsFrameTranslateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java index 997e0e27ef9c..b16f41234656 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java @@ -41,8 +41,8 @@ import android.view.MotionEvent; import androidx.test.filters.SmallTest; -import com.android.systemui.res.R; import com.android.systemui.plugins.qs.QS; +import com.android.systemui.res.R; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,7 +53,7 @@ import java.util.List; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class QuickSettingsControllerTest extends QuickSettingsControllerBaseTest { +public class QuickSettingsControllerImplTest extends QuickSettingsControllerImplBaseTest { @Test public void testCloseQsSideEffects() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt index cc4a06341cc6..2c453a711c87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt @@ -27,7 +27,7 @@ import org.junit.Test @SmallTest @OptIn(ExperimentalCoroutinesApi::class) -class QuickSettingsControllerWithCoroutinesTest : QuickSettingsControllerBaseTest() { +class QuickSettingsControllerImplWithCoroutinesTest : QuickSettingsControllerImplBaseTest() { @Test fun isExpansionEnabled_dozing_false() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt index b3fc25c47912..24195fe0640c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt @@ -16,8 +16,10 @@ package com.android.systemui.statusbar.notification.icon.ui.viewmodel +import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION import com.android.systemui.SysUITestComponent import com.android.systemui.SysUITestModule @@ -238,6 +240,7 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun animationsEnabled_isTrue_whenKeyguardIsShowing() = testComponent.runTest { keyguardTransitionRepository.sendTransitionStep( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index e78081fc34bd..fb49499fc29d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -89,6 +89,8 @@ import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; import com.android.systemui.statusbar.policy.SmartReplyConstants; import com.android.systemui.statusbar.policy.SmartReplyStateInflater; import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; +import com.android.systemui.util.time.SystemClock; +import com.android.systemui.util.time.SystemClockImpl; import com.android.systemui.wmshell.BubblesManager; import com.android.systemui.wmshell.BubblesTestActivity; @@ -136,6 +138,8 @@ public class NotificationTestHelper { public final Runnable mFutureDismissalRunnable; private @InflationFlag int mDefaultInflationFlags; private final FakeFeatureFlags mFeatureFlags; + private final SystemClock mSystemClock; + private final RowInflaterTaskLogger mRowInflaterTaskLogger; public NotificationTestHelper( Context context, @@ -199,6 +203,9 @@ public class NotificationTestHelper { mFutureDismissalRunnable = mock(Runnable.class); when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt())) .thenReturn(mFutureDismissalRunnable); + + mSystemClock = new SystemClockImpl(); + mRowInflaterTaskLogger = mock(RowInflaterTaskLogger.class); } public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) { @@ -572,7 +579,8 @@ public class NotificationTestHelper { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); if (com.android.systemui.Flags.notificationRowUserContext()) { - inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry)); + inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock, + mRowInflaterTaskLogger)); } mRow = (ExpandableNotificationRow) inflater.inflate( R.layout.status_bar_notification_row, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt new file mode 100644 index 000000000000..f88bd7ec60bb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.statusbar.notification.shared.NotificationViewFlipperPausing +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(NotificationViewFlipperPausing.FLAG_NAME) +class NotificationViewFlipperViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + val underTest + get() = kosmos.notificationViewFlipperViewModel + + @Test + fun testIsPaused_falseWhenViewingShade() = + kosmos.testScope.runTest { + val isPaused by collectLastValue(underTest.isPaused) + + // WHEN shade is open + kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + runCurrent() + + // THEN view flippers should NOT be paused + assertThat(isPaused).isFalse() + } + + @Test + fun testIsPaused_trueWhenViewingKeyguard() = + kosmos.testScope.runTest { + val isPaused by collectLastValue(underTest.isPaused) + + // WHEN on keyguard + kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + runCurrent() + + // THEN view flippers should be paused + assertThat(isPaused).isTrue() + } + + @Test + fun testIsPaused_trueWhenStartingToSleep() = + kosmos.testScope.runTest { + val isPaused by collectLastValue(underTest.isPaused) + + // WHEN shade is open + kosmos.fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + // AND device is starting to go to sleep + kosmos.fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP) + runCurrent() + + // THEN view flippers should be paused + assertThat(isPaused).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index f050857d5df2..562aa6a4f497 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -95,6 +95,7 @@ import com.android.systemui.shade.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewManagerInteractor; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -226,7 +227,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mSelectedUserInteractor, () -> mock(KeyguardSurfaceBehindInteractor.class), mock(JavaAdapter.class), - () -> mock(SceneInteractor.class)) { + () -> mock(SceneInteractor.class), + mock(StatusBarKeyguardViewManagerInteractor.class)) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -736,7 +738,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mSelectedUserInteractor, () -> mock(KeyguardSurfaceBehindInteractor.class), mock(JavaAdapter.class), - () -> mock(SceneInteractor.class)) { + () -> mock(SceneInteractor.class), + mock(StatusBarKeyguardViewManagerInteractor.class)) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java index 1dafcc48f19f..b0404a055a68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java @@ -15,13 +15,14 @@ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; + import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE; import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK; import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt index c3f677e8d0d3..d5411ad77ce4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.activityStarter @@ -40,5 +41,6 @@ val Kosmos.occludingAppDeviceEntryInteractor by context = mockedContext, activityStarter = activityStarter, powerInteractor = powerInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index a9a2d91c0815..dcbd5777489a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import android.annotation.FloatRange +import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -65,55 +66,79 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio } /** - * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling - * [runCurrent] after each step. + * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. + * + * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part + * way using [throughTransitionState]. */ suspend fun sendTransitionSteps( from: KeyguardState, to: KeyguardState, testScope: TestScope, + throughTransitionState: TransitionState = TransitionState.FINISHED, ) { - sendTransitionSteps(from, to, testScope.testScheduler) + sendTransitionSteps(from, to, testScope.testScheduler, throughTransitionState) } /** - * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling - * [runCurrent] after each step. + * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. + * + * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part + * way using [throughTransitionState]. */ suspend fun sendTransitionSteps( from: KeyguardState, to: KeyguardState, testScheduler: TestCoroutineScheduler, + throughTransitionState: TransitionState = TransitionState.FINISHED, ) { sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - from = from, - to = to, - value = 0f, - ) + step = + TransitionStep( + transitionState = TransitionState.STARTED, + from = from, + to = to, + value = 0f, + ) ) testScheduler.runCurrent() - sendTransitionStep( - TransitionStep( - transitionState = TransitionState.RUNNING, - from = from, - to = to, - value = 0.5f + if ( + throughTransitionState == TransitionState.RUNNING || + throughTransitionState == TransitionState.FINISHED + ) { + sendTransitionStep( + step = + TransitionStep( + transitionState = TransitionState.RUNNING, + from = from, + to = to, + value = 0.5f + ) ) - ) - testScheduler.runCurrent() + testScheduler.runCurrent() + } - sendTransitionStep( - TransitionStep( - transitionState = TransitionState.FINISHED, - from = from, - to = to, - value = 1f, + if (throughTransitionState == TransitionState.FINISHED) { + sendTransitionStep( + step = + TransitionStep( + transitionState = TransitionState.FINISHED, + from = from, + to = to, + value = 1f, + ) ) + testScheduler.runCurrent() + } + } + + suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) { + this.sendTransitionStep( + step = step, + validateStep = validateStep, + ownerName = step.ownerName ) - testScheduler.runCurrent() } /** @@ -132,7 +157,22 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio * If you're testing something involving transitions themselves and are sure you want to send * only a FINISHED step, override [validateStep]. */ - suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) { + suspend fun sendTransitionStep( + from: KeyguardState = KeyguardState.OFF, + to: KeyguardState = KeyguardState.OFF, + value: Float = 0f, + transitionState: TransitionState = TransitionState.FINISHED, + ownerName: String = "", + step: TransitionStep = + TransitionStep( + from = from, + to = to, + value = value, + transitionState = transitionState, + ownerName = ownerName + ), + validateStep: Boolean = true + ) { _transitions.replayCache.last().let { lastStep -> if ( validateStep && @@ -159,7 +199,9 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio step: TransitionStep, validateStep: Boolean = true ): Job { - return coroutineScope.launch { sendTransitionStep(step, validateStep) } + return coroutineScope.launch { + sendTransitionStep(step = step, validateStep = validateStep) + } } suspend fun sendTransitionSteps( @@ -168,12 +210,13 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio validateStep: Boolean = true ) { steps.forEach { - sendTransitionStep(it, validateStep = validateStep) + sendTransitionStep(step = it, validateStep = validateStep) testScope.testScheduler.runCurrent() } } override fun startTransition(info: TransitionInfo): UUID? { + Log.i("TEST", "Start transition: ", Exception()) return if (info.animator == null) UUID.randomUUID() else null } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt new file mode 100644 index 000000000000..4c8bf9054106 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardOcclusionRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyguardOcclusionRepository by Kosmos.Fixture { KeyguardOcclusionRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..530cbedbdd0c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +val Kosmos.fromAlternateBouncerTransitionInteractor by + Kosmos.Fixture { + FromAlternateBouncerTransitionInteractor( + transitionRepository = keyguardTransitionRepository, + transitionInteractor = keyguardTransitionInteractor, + scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt index 2477415cc06e..bbe37c18dd08 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt @@ -18,19 +18,21 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor val Kosmos.fromAodTransitionInteractor by Kosmos.Fixture { FromAodTransitionInteractor( transitionRepository = fakeKeyguardTransitionRepository, transitionInteractor = keyguardTransitionInteractor, - scope = testScope, + scope = applicationCoroutineScope, bgDispatcher = testDispatcher, mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..23dcd965c028 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +var Kosmos.fromDozingTransitionInteractor by + Kosmos.Fixture { + FromDozingTransitionInteractor( + transitionRepository = keyguardTransitionRepository, + transitionInteractor = keyguardTransitionInteractor, + scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..f7a9d59eac26 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractorKosmos.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +var Kosmos.fromDreamingLockscreenHostedTransitionInteractor by + Kosmos.Fixture { + FromDreamingLockscreenHostedTransitionInteractor( + transitionRepository = keyguardTransitionRepository, + transitionInteractor = keyguardTransitionInteractor, + scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + keyguardInteractor = keyguardInteractor, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..135644cfac3e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorKosmos.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +var Kosmos.fromDreamingTransitionInteractor by + Kosmos.Fixture { + FromDreamingTransitionInteractor( + transitionRepository = keyguardTransitionRepository, + transitionInteractor = keyguardTransitionInteractor, + scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + keyguardInteractor = keyguardInteractor, + glanceableHubTransitions = glanceableHubTransitions, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..1695327d75bc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +var Kosmos.fromGlanceableHubTransitionInteractor by + Kosmos.Fixture { + FromGlanceableHubTransitionInteractor( + transitionRepository = keyguardTransitionRepository, + transitionInteractor = keyguardTransitionInteractor, + scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + keyguardInteractor = keyguardInteractor, + powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + glanceableHubTransitions = glanceableHubTransitions, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt index 25fc67a9691b..604d9e435e8e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt @@ -17,11 +17,13 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor val Kosmos.fromGoneTransitionInteractor by Kosmos.Fixture { @@ -34,5 +36,7 @@ val Kosmos.fromGoneTransitionInteractor by keyguardInteractor = keyguardInteractor, powerInteractor = powerInteractor, communalInteractor = communalInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + biometricSettingsRepository = biometricSettingsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt index 3b52676fc0ef..162fd9029bb0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor var Kosmos.fromLockscreenTransitionInteractor by Kosmos.Fixture { @@ -38,5 +39,6 @@ var Kosmos.fromLockscreenTransitionInteractor by powerInteractor = powerInteractor, glanceableHubTransitions = glanceableHubTransitions, swipeToDismissInteractor = swipeToDismissInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..fc740a180dc4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor + +val Kosmos.fromOccludedTransitionInteractor by + Kosmos.Fixture { + FromOccludedTransitionInteractor( + transitionRepository = keyguardTransitionRepository, + transitionInteractor = keyguardTransitionInteractor, + scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + keyguardInteractor = keyguardInteractor, + powerInteractor = powerInteractor, + communalInteractor = communalInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt index 6b764491f32a..98babffb50d3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.user.domain.interactor.selectedUserInteractor var Kosmos.fromPrimaryBouncerTransitionInteractor by @@ -40,5 +41,6 @@ var Kosmos.fromPrimaryBouncerTransitionInteractor by keyguardSecurityModel = keyguardSecurityModel, selectedUserInteractor = selectedUserInteractor, powerInteractor = powerInteractor, + keyguardOcclusionInteractor = keyguardOcclusionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt index 6df7493be200..6cc1e8eba73d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt @@ -16,19 +16,21 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import dagger.Lazy val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by Kosmos.Fixture { KeyguardTransitionInteractor( scope = applicationCoroutineScope, repository = keyguardTransitionRepository, - fromLockscreenTransitionInteractor = Lazy { fromLockscreenTransitionInteractor }, - fromPrimaryBouncerTransitionInteractor = - Lazy { fromPrimaryBouncerTransitionInteractor }, - fromAodTransitionInteractor = Lazy { fromAodTransitionInteractor }, + keyguardRepository = keyguardRepository, + fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor }, + fromPrimaryBouncerTransitionInteractor = { fromPrimaryBouncerTransitionInteractor }, + fromAodTransitionInteractor = { fromAodTransitionInteractor }, + fromAlternateBouncerTransitionInteractor = { fromAlternateBouncerTransitionInteractor }, + fromDozingTransitionInteractor = { fromDozingTransitionInteractor }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..8162520e5d88 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.dozingToOccludedTransitionViewModel by + Kosmos.Fixture { + DozingToOccludedTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index c2300a1ed1ad..75e3ac24e381 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.keyguard.ui.viewmodel @@ -41,8 +40,10 @@ val Kosmos.keyguardRootViewModel by Fixture { alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, aodToGoneTransitionViewModel = aodToGoneTransitionViewModel, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, + aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, + dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel, glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, goneToAodTransitionViewModel = goneToAodTransitionViewModel, diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt index 920e5ee94cca..41d2d600e627 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeActivityTaskManager.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeActivityTaskManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.mediaprojection.taskswitcher.data.repository +package com.android.systemui.mediaprojection.taskswitcher import android.app.ActivityManager.RunningTaskInfo import android.app.IActivityTaskManager diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt index 28393e837b93..2b6032ccafe5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/FakeMediaProjectionManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.mediaprojection.taskswitcher.data.repository +package com.android.systemui.mediaprojection.taskswitcher import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt new file mode 100644 index 000000000000..d344b75c9eb7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.taskswitcher + +import android.os.Handler +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository +import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository +import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor +import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +val Kosmos.fakeActivityTaskManager by Kosmos.Fixture { FakeActivityTaskManager() } + +val Kosmos.fakeMediaProjectionManager by Kosmos.Fixture { FakeMediaProjectionManager() } + +val Kosmos.activityTaskManagerTasksRepository by + Kosmos.Fixture { + ActivityTaskManagerTasksRepository( + activityTaskManager = fakeActivityTaskManager.activityTaskManager, + applicationScope = applicationCoroutineScope, + backgroundDispatcher = testDispatcher + ) + } + +val Kosmos.mediaProjectionManagerRepository by + Kosmos.Fixture { + MediaProjectionManagerRepository( + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = applicationCoroutineScope, + tasksRepository = activityTaskManagerTasksRepository, + backgroundDispatcher = testDispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, + ) + } + +val Kosmos.taskSwitcherInteractor by + Kosmos.Fixture { + TaskSwitchInteractor(mediaProjectionManagerRepository, activityTaskManagerTasksRepository) + } + +val Kosmos.taskSwitcherViewModel by + Kosmos.Fixture { TaskSwitcherNotificationViewModel(taskSwitcherInteractor, testDispatcher) } + +@OptIn(ExperimentalCoroutinesApi::class) +fun taskSwitcherKosmos() = Kosmos().apply { testDispatcher = UnconfinedTestDispatcher() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt new file mode 100644 index 000000000000..d79374021968 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.domain.interactor + +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.powerInteractor + +val Kosmos.keyguardOcclusionInteractor by + Kosmos.Fixture { + KeyguardOcclusionInteractor( + scope = testScope.backgroundScope, + repository = keyguardOcclusionRepository, + powerInteractor = powerInteractor, + transitionInteractor = keyguardTransitionInteractor, + keyguardInteractor = keyguardInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt new file mode 100644 index 000000000000..9e34fe8d7c61 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardViewOcclusionInteractorKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.domain.interactor + +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.power.domain.interactor.powerInteractor + +val Kosmos.statusBarKeyguardViewManagerInteractor by + Kosmos.Fixture { + StatusBarKeyguardViewManagerInteractor( + keyguardTransitionInteractor = this.keyguardTransitionInteractor, + keyguardOcclusionInteractor = this.keyguardOcclusionInteractor, + powerInteractor = this.powerInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt new file mode 100644 index 000000000000..7ffa262d55a4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/NotificationViewFlipperViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor + +val Kosmos.notificationViewFlipperViewModel by Fixture { + NotificationViewFlipperViewModel( + dumpManager = dumpManager, + stackInteractor = notificationStackInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 106e85cc8d85..c01366489c69 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -23,7 +23,9 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.goneToAodTransitionViewModel @@ -57,7 +59,9 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { communalInteractor = communalInteractor, alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, + aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, + dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel, goneToAodTransitionViewModel = goneToAodTransitionViewModel, goneToDozingTransitionViewModel = goneToDozingTransitionViewModel, diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java index cc94090d9ec7..9057d163957e 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java @@ -42,7 +42,6 @@ public class ClassLoadHook { public static final String KEYBOARD_PATHS = "keyboard_paths"; public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes"; - public static final String VALUE_N_A = "**n/a**"; public static final String LIBANDROID_RUNTIME_NAME = "android_runtime"; private static String sInitialDir = new File("").getAbsolutePath(); @@ -130,8 +129,6 @@ public class ClassLoadHook { } setProperty(CORE_NATIVE_CLASSES, jniClasses); setProperty(GRAPHICS_NATIVE_CLASSES, ""); - setProperty(ICU_DATA_PATH, VALUE_N_A); - setProperty(KEYBOARD_PATHS, VALUE_N_A); RavenwoodUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME); } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index a754ba547767..997f3af3533e 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -59,6 +59,13 @@ flag { } flag { + name: "enable_magnification_one_finger_panning_gesture" + namespace: "accessibility" + description: "Whether to allow easy-mode (one finger panning gesture) for magnification" + bug: "282039824" +} + +flag { name: "fix_drag_pointer_when_ending_drag" namespace: "accessibility" description: "Send the correct pointer id when transitioning from dragging to delegating states." diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index 279bd72da6e7..6d1ab9f89f78 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -167,7 +167,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH }) public @interface OverscrollState {} - @VisibleForTesting boolean mIsSinglePanningEnabled; + @VisibleForTesting final OneFingerPanningSettingsProvider mOneFingerPanningSettingsProvider; private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper; @@ -201,7 +201,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH displayId, fullScreenMagnificationVibrationHelper, /* magnificationLogger= */ null, - ViewConfiguration.get(context)); + ViewConfiguration.get(context), + new OneFingerPanningSettingsProvider( + context, + Flags.enableMagnificationOneFingerPanningGesture() + )); } /** Constructor for tests. */ @@ -218,7 +222,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH int displayId, FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper, MagnificationLogger magnificationLogger, - ViewConfiguration viewConfiguration) { + ViewConfiguration viewConfiguration, + OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider + ) { super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap, detectShortcutTrigger, trace, callback); if (DEBUG_ALL) { @@ -301,9 +307,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH mPanningScalingState = new PanningScalingState(context); mSinglePanningState = new SinglePanningState(context); mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper; - setSinglePanningEnabled( - context.getResources() - .getBoolean(R.bool.config_enable_a11y_magnification_single_panning)); + mOneFingerPanningSettingsProvider = oneFingerPanningSettingsProvider; mOverscrollHandler = new OverscrollHandler(); mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); @@ -317,11 +321,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH transitionTo(mDetectingState); } - @VisibleForTesting - void setSinglePanningEnabled(boolean isEnabled) { - mIsSinglePanningEnabled = isEnabled; - } - @Override void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) { if (event.getActionMasked() == ACTION_DOWN) { @@ -361,6 +360,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH Slog.i(mLogTag, "onDestroy(); delayed = " + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue)); } + mOneFingerPanningSettingsProvider.unregister(); if (mScreenStateReceiver != null) { mScreenStateReceiver.unregister(); @@ -524,7 +524,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH && event.getPointerCount() == 2 // includes the pointer currently being released && mPreviousState == mViewportDraggingState) { // if feature flag is enabled, currently only true on watches - if (mIsSinglePanningEnabled) { + if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) { mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded(); mOverscrollHandler.clearEdgeState(); } @@ -532,7 +532,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } else if (action == ACTION_UP || action == ACTION_CANCEL) { onPanningFinished(event); // if feature flag is enabled, currently only true on watches - if (mIsSinglePanningEnabled) { + if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) { mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded(); mOverscrollHandler.clearEdgeState(); } @@ -611,7 +611,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH onPan(second); mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX, distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); - if (mIsSinglePanningEnabled) { + if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()) { mOverscrollHandler.onScrollStateChanged(first, second); } return /* event consumed: */ true; @@ -1000,7 +1000,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH && event.getPointerCount() == 2) { transitionToViewportDraggingStateAndClear(event); } else if (isActivated() && event.getPointerCount() == 2) { - if (mIsSinglePanningEnabled + if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() && overscrollState(event, mFirstPointerDownLocation) == OVERSCROLL_VERTICAL_EDGE) { transitionToDelegatingStateAndClear(); @@ -1008,7 +1008,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH //Primary pointer is swiping, so transit to PanningScalingState transitToPanningScalingStateAndClear(); } - } else if (mIsSinglePanningEnabled + } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() && isActivated() && event.getPointerCount() == 1) { if (overscrollState(event, mFirstPointerDownLocation) @@ -1255,7 +1255,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) { transitionToViewportDraggingStateAndClear(event); } else if (isActivated() && event.getPointerCount() == 2) { - if (mIsSinglePanningEnabled + if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() && overscrollState(event, mFirstPointerDownLocation) == OVERSCROLL_VERTICAL_EDGE) { transitionToDelegatingStateAndClear(); @@ -1263,7 +1263,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH //Primary pointer is swiping, so transit to PanningScalingState transitToPanningScalingStateAndClear(); } - } else if (mIsSinglePanningEnabled + } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() && isActivated() && event.getPointerCount() == 1) { if (overscrollState(event, mFirstPointerDownLocation) @@ -1633,7 +1633,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH + ", mPreviousState=" + State.nameOf(mPreviousState) + ", mMagnificationController=" + mFullScreenMagnificationController + ", mDisplayId=" + mDisplayId - + ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled + + ", mIsSinglePanningEnabled=" + + mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() + ", mOverscrollHandler=" + mOverscrollHandler + '}'; } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OneFingerPanningSettingsProvider.java b/services/accessibility/java/com/android/server/accessibility/magnification/OneFingerPanningSettingsProvider.java new file mode 100644 index 000000000000..3cdaf98780d9 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/magnification/OneFingerPanningSettingsProvider.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 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.accessibility.magnification; + + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provider for secure settings {@link Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED}. + */ +public class OneFingerPanningSettingsProvider { + + @VisibleForTesting + static final String KEY = Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED; + private static final Uri URI = Settings.Secure.getUriFor(KEY); + private AtomicBoolean mCached = new AtomicBoolean(); + @VisibleForTesting + ContentObserver mObserver; + @VisibleForTesting + ContentResolver mContentResolver; + + @Retention(RetentionPolicy.SOURCE) + public @interface State { + int OFF = 0; + int ON = 1; + } + + public OneFingerPanningSettingsProvider( + Context context, + boolean featureFlagEnabled + ) { + var defaultValue = isOneFingerPanningEnabledDefault(context); + if (featureFlagEnabled) { + mContentResolver = context.getContentResolver(); + mObserver = new ContentObserver(context.getMainThreadHandler()) { + @Override + public void onChange(boolean selfChange) { + mCached.set(isOneFingerPanningEnabledInSetting(context, defaultValue)); + } + }; + mCached.set(isOneFingerPanningEnabledInSetting(context, defaultValue)); + mContentResolver.registerContentObserver(URI, false, mObserver); + } else { + mCached.set(defaultValue); + } + } + + /** Returns whether one finger panning is enabled.. */ + public boolean isOneFingerPanningEnabled() { + return mCached.get(); + } + + /** Unregister content observer for listening to secure settings. */ + public void unregister() { + if (mContentResolver != null) { + mContentResolver.unregisterContentObserver(mObserver); + } + mContentResolver = null; + } + + private boolean isOneFingerPanningEnabledInSetting(Context context, boolean defaultValue) { + return State.ON == Settings.Secure.getIntForUser( + mContentResolver, + KEY, + (defaultValue ? State.ON : State.OFF), + context.getUserId()); + } + + @VisibleForTesting + static boolean isOneFingerPanningEnabledDefault(Context context) { + boolean oneFingerPanningDefaultValue; + try { + oneFingerPanningDefaultValue = context.getResources().getBoolean( + com.android.internal.R.bool.config_enable_a11y_magnification_single_panning); + } catch (Resources.NotFoundException e) { + oneFingerPanningDefaultValue = false; + } + return oneFingerPanningDefaultValue; + } +} diff --git a/services/companion/java/com/android/server/companion/AssociationStore.java b/services/companion/java/com/android/server/companion/AssociationStore.java deleted file mode 100644 index 01905bb2297f..000000000000 --- a/services/companion/java/com/android/server/companion/AssociationStore.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2021 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.companion; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.companion.AssociationInfo; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Collection; -import java.util.List; - -/** - * Interface for a store of {@link AssociationInfo}-s. - */ -public interface AssociationStore { - - @IntDef(prefix = { "CHANGE_TYPE_" }, value = { - CHANGE_TYPE_ADDED, - CHANGE_TYPE_REMOVED, - CHANGE_TYPE_UPDATED_ADDRESS_CHANGED, - CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED, - }) - @Retention(RetentionPolicy.SOURCE) - @interface ChangeType {} - - int CHANGE_TYPE_ADDED = 0; - int CHANGE_TYPE_REMOVED = 1; - int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2; - int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3; - - /** Listener for any changes to {@link AssociationInfo}-s. */ - interface OnChangeListener { - default void onAssociationChanged( - @ChangeType int changeType, AssociationInfo association) { - switch (changeType) { - case CHANGE_TYPE_ADDED: - onAssociationAdded(association); - break; - - case CHANGE_TYPE_REMOVED: - onAssociationRemoved(association); - break; - - case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED: - onAssociationUpdated(association, true); - break; - - case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED: - onAssociationUpdated(association, false); - break; - } - } - - default void onAssociationAdded(AssociationInfo association) {} - - default void onAssociationRemoved(AssociationInfo association) {} - - default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {} - } - - /** - * @return all CDM associations. - */ - @NonNull - Collection<AssociationInfo> getAssociations(); - - /** - * @return a {@link List} of associations that belong to the user. - */ - @NonNull - List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId); - - /** - * @return a {@link List} of association that belong to the package. - */ - @NonNull - List<AssociationInfo> getAssociationsForPackage( - @UserIdInt int userId, @NonNull String packageName); - - /** - * @return an association with the given address that belong to the given package if such an - * association exists, otherwise {@code null}. - */ - @Nullable - AssociationInfo getAssociationsForPackageWithAddress( - @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress); - - /** - * @return an association with the given id if such an association exists, otherwise - * {@code null}. - */ - @Nullable - AssociationInfo getAssociationById(int id); - - /** - * @return all associations with the given MAc address. - */ - @NonNull - List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress); - - /** Register a {@link OnChangeListener} */ - void registerListener(@NonNull OnChangeListener listener); - - /** Un-register a previously registered {@link OnChangeListener} */ - void unregisterListener(@NonNull OnChangeListener listener); - - /** @hide */ - static String changeTypeToString(@ChangeType int changeType) { - switch (changeType) { - case CHANGE_TYPE_ADDED: - return "ASSOCIATION_ADDED"; - - case CHANGE_TYPE_REMOVED: - return "ASSOCIATION_REMOVED"; - - case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED: - return "ASSOCIATION_UPDATED"; - - case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED: - return "ASSOCIATION_UPDATED_ADDRESS_UNCHANGED"; - - default: - return "Unknown (" + changeType + ")"; - } - } -} diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java index e4cc1f8949b5..f2409fb8843e 100644 --- a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java +++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java @@ -34,6 +34,9 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; +import com.android.server.companion.association.AssociationDiskStore; +import com.android.server.companion.association.AssociationRequestsProcessor; +import com.android.server.companion.association.AssociationStore; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import java.nio.ByteBuffer; @@ -54,9 +57,9 @@ class BackupRestoreProcessor { @NonNull private final PackageManagerInternal mPackageManager; @NonNull - private final AssociationStoreImpl mAssociationStore; + private final AssociationStore mAssociationStore; @NonNull - private final PersistentDataStore mPersistentStore; + private final AssociationDiskStore mPersistentStore; @NonNull private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; @NonNull @@ -71,8 +74,8 @@ class BackupRestoreProcessor { new PerUserAssociationSet(); BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service, - @NonNull AssociationStoreImpl associationStore, - @NonNull PersistentDataStore persistentStore, + @NonNull AssociationStore associationStore, + @NonNull AssociationDiskStore persistentStore, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull AssociationRequestsProcessor associationRequestsProcessor) { mService = service; diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java index 559ebbc290f6..c801489ce963 100644 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java @@ -37,6 +37,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.PerUser; +import com.android.server.companion.association.AssociationStore; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.presence.ObservableUuidStore; diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 17ba0730c8e4..e4a1048e9faa 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -37,7 +37,9 @@ import static android.os.UserHandle.getCallingUserId; import static com.android.internal.util.CollectionUtils.any; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; -import static com.android.server.companion.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED; +import static com.android.server.companion.association.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED; +import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser; +import static com.android.server.companion.utils.AssociationUtils.getLastAssociationIdForUser; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PackageUtils.getPackageInfo; @@ -117,6 +119,11 @@ import com.android.internal.util.DumpUtils; import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.companion.association.AssociationDiskStore; +import com.android.server.companion.association.AssociationRequestsProcessor; +import com.android.server.companion.association.AssociationRevokeProcessor; +import com.android.server.companion.association.AssociationStore; +import com.android.server.companion.association.InactiveAssociationsRemovalService; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; @@ -147,8 +154,6 @@ public class CompanionDeviceManagerService extends SystemService { static final String TAG = "CDM_CompanionDeviceManagerService"; static final boolean DEBUG = false; - /** Range of Association IDs allocated for a user. */ - private static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000; private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; @@ -160,10 +165,10 @@ public class CompanionDeviceManagerService extends SystemService { private static final int MAX_CN_LENGTH = 500; private final ActivityManager mActivityManager; - private PersistentDataStore mPersistentStore; + private AssociationDiskStore mAssociationDiskStore; private final PersistUserStateHandler mUserPersistenceHandler; - private final AssociationStoreImpl mAssociationStore; + private final AssociationStore mAssociationStore; private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; private AssociationRequestsProcessor mAssociationRequestsProcessor; private SystemDataTransferProcessor mSystemDataTransferProcessor; @@ -178,7 +183,7 @@ public class CompanionDeviceManagerService extends SystemService { private final IAppOpsService mAppOpsManager; private final PowerWhitelistManager mPowerWhitelistManager; private final UserManager mUserManager; - final PackageManagerInternal mPackageManagerInternal; + public final PackageManagerInternal mPackageManagerInternal; private final PowerManagerInternal mPowerManagerInternal; /** @@ -210,7 +215,7 @@ public class CompanionDeviceManagerService extends SystemService { mUserManager = context.getSystemService(UserManager.class); mUserPersistenceHandler = new PersistUserStateHandler(); - mAssociationStore = new AssociationStoreImpl(); + mAssociationStore = new AssociationStore(); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); @@ -221,11 +226,11 @@ public class CompanionDeviceManagerService extends SystemService { public void onStart() { final Context context = getContext(); - mPersistentStore = new PersistentDataStore(); + mAssociationDiskStore = new AssociationDiskStore(); mAssociationRequestsProcessor = new AssociationRequestsProcessor( /* cdmService */ this, mAssociationStore); mBackupRestoreProcessor = new BackupRestoreProcessor( - /* cdmService */ this, mAssociationStore, mPersistentStore, + /* cdmService */ this, mAssociationStore, mAssociationDiskStore, mSystemDataTransferRequestStore, mAssociationRequestsProcessor); mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId()); @@ -264,10 +269,13 @@ public class CompanionDeviceManagerService extends SystemService { void loadAssociationsFromDisk() { final Set<AssociationInfo> allAssociations = new ArraySet<>(); synchronized (mPreviouslyUsedIds) { + List<Integer> userIds = new ArrayList<>(); + for (UserInfo user : mUserManager.getAliveUsers()) { + userIds.add(user.id); + } // The data is stored in DE directories, so we can read the data for all users now // (which would not be possible if the data was stored to CE directories). - mPersistentStore.readStateForUsers( - mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds); + mAssociationDiskStore.readStateForUsers(userIds, allAssociations, mPreviouslyUsedIds); } final Set<AssociationInfo> activeAssociations = @@ -291,7 +299,7 @@ public class CompanionDeviceManagerService extends SystemService { } } - mAssociationStore.setAssociations(activeAssociations); + mAssociationStore.setAssociationsToCache(activeAssociations); // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because // persistStateForUser() queries AssociationStore. @@ -582,7 +590,7 @@ public class CompanionDeviceManagerService extends SystemService { final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId); - mPersistentStore.persistStateForUser(userId, allAssociations, usedIdsForUser); + mAssociationDiskStore.persistStateForUser(userId, allAssociations, usedIdsForUser); } private void notifyListeners( @@ -646,7 +654,8 @@ public class CompanionDeviceManagerService extends SystemService { final List<AssociationInfo> associationsForPackage = mAssociationStore.getAssociationsForPackage(userId, packageName); for (AssociationInfo association : associationsForPackage) { - updateSpecialAccessPermissionForAssociatedPackage(association); + updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), + association.getPackageName()); } mCompanionAppController.onPackagesChanged(userId); @@ -692,7 +701,7 @@ public class CompanionDeviceManagerService extends SystemService { } } - class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { + public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { @@ -1338,7 +1347,10 @@ public class CompanionDeviceManagerService extends SystemService { return usedIdsForPackage; } - int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) { + /** + * Get a new association id for the package. + */ + public int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) { synchronized (mPreviouslyUsedIds) { // First: collect all IDs currently in use for this user's Associations. final SparseBooleanArray usedIds = new SparseBooleanArray(); @@ -1383,9 +1395,12 @@ public class CompanionDeviceManagerService extends SystemService { } } - void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) { + /** + * Update special access for the association's package + */ + public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) { final PackageInfo packageInfo = - getPackageInfo(getContext(), association.getUserId(), association.getPackageName()); + getPackageInfo(getContext(), userId, packageName); Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo)); } @@ -1539,15 +1554,6 @@ public class CompanionDeviceManagerService extends SystemService { } }; - static int getFirstAssociationIdForUser(@UserIdInt int userId) { - // We want the IDs to start from 1, not 0. - return userId * ASSOCIATIONS_IDS_PER_USER_RANGE + 1; - } - - static int getLastAssociationIdForUser(@UserIdInt int userId) { - return (userId + 1) * ASSOCIATIONS_IDS_PER_USER_RANGE; - } - private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) { final Map<String, Set<Integer>> copy = new HashMap<>(); @@ -1671,11 +1677,17 @@ public class CompanionDeviceManagerService extends SystemService { } } - void postPersistUserState(@UserIdInt int userId) { + /** + * Persist associations + */ + public void postPersistUserState(@UserIdInt int userId) { mUserPersistenceHandler.postPersistUserState(userId); } - static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> { + /** + * Set to store associations + */ + public 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/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 74b4cabbab67..16877dcaf183 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -32,6 +32,9 @@ import android.os.ShellCommand; import android.util.Base64; import android.util.proto.ProtoOutputStream; +import com.android.server.companion.association.AssociationRequestsProcessor; +import com.android.server.companion.association.AssociationRevokeProcessor; +import com.android.server.companion.association.AssociationStore; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; @@ -47,7 +50,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private final CompanionDeviceManagerService mService; private final AssociationRevokeProcessor mRevokeProcessor; - private final AssociationStoreImpl mAssociationStore; + private final AssociationStore mAssociationStore; private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; private final CompanionTransportManager mTransportManager; @@ -56,7 +59,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private final BackupRestoreProcessor mBackupRestoreProcessor; CompanionDeviceShellCommand(CompanionDeviceManagerService service, - AssociationStoreImpl associationStore, + AssociationStore associationStore, CompanionDevicePresenceMonitor devicePresenceMonitor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java index 7527efb7b19a..75cb12058247 100644 --- a/services/companion/java/com/android/server/companion/PersistentDataStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.association; import static com.android.internal.util.CollectionUtils.forEach; import static com.android.internal.util.XmlUtils.readBooleanAttribute; @@ -25,8 +25,8 @@ import static com.android.internal.util.XmlUtils.writeBooleanAttribute; import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.internal.util.XmlUtils.writeLongAttribute; import static com.android.internal.util.XmlUtils.writeStringAttribute; -import static com.android.server.companion.CompanionDeviceManagerService.getFirstAssociationIdForUser; -import static com.android.server.companion.CompanionDeviceManagerService.getLastAssociationIdForUser; +import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser; +import static com.android.server.companion.utils.AssociationUtils.getLastAssociationIdForUser; import static com.android.server.companion.utils.DataStoreUtils.createStorageFileForUser; import static com.android.server.companion.utils.DataStoreUtils.fileToByteArray; import static com.android.server.companion.utils.DataStoreUtils.isEndOfTag; @@ -38,12 +38,10 @@ import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; -import android.content.pm.UserInfo; import android.net.MacAddress; import android.os.Environment; import android.util.ArrayMap; import android.util.AtomicFile; -import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; @@ -51,7 +49,6 @@ import android.util.Xml; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; -import com.android.server.companion.utils.DataStoreUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -71,6 +68,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** + * IMPORTANT: This class should NOT be directly used except {@link AssociationStore} + * * The class responsible for persisting Association records and other related information (such as * previously used IDs) to a disk, and reading the data back from the disk. * @@ -107,8 +106,6 @@ import java.util.concurrent.ConcurrentMap; * Since Android T the data is stored to "companion_device_manager.xml" file in * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}. * - * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)} - * * <p> * Since Android T the data is stored using the v1 schema. * @@ -161,9 +158,8 @@ import java.util.concurrent.ConcurrentMap; * }</pre> */ @SuppressLint("LongLogTag") -final class PersistentDataStore { - private static final String TAG = "CompanionDevice_PersistentDataStore"; - private static final boolean DEBUG = CompanionDeviceManagerService.DEBUG; +public final class AssociationDiskStore { + private static final String TAG = "CompanionDevice_AssociationDiskStore"; private static final int CURRENT_PERSISTENCE_VERSION = 1; @@ -200,11 +196,13 @@ final class PersistentDataStore { private final @NonNull ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile = new ConcurrentHashMap<>(); - void readStateForUsers(@NonNull List<UserInfo> users, + /** + * Read all associations for given users + */ + public void readStateForUsers(@NonNull List<Integer> userIds, @NonNull Set<AssociationInfo> allAssociationsOut, @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut) { - for (UserInfo user : users) { - final int userId = user.id; + for (int userId : userIds) { // Previously used IDs are stored in the "out" collection per-user. final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>(); @@ -247,12 +245,11 @@ final class PersistentDataStore { * @param associationsOut a container to read the {@link AssociationInfo}s "into". * @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into". */ - void readStateForUser(@UserIdInt int userId, + private void readStateForUser(@UserIdInt int userId, @NonNull Collection<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { Slog.i(TAG, "Reading associations for user " + userId + " from disk"); final AtomicFile file = getStorageFileForUser(userId); - if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize // accesses to the file on the file system using this AtomicFile object. @@ -261,12 +258,8 @@ final class PersistentDataStore { final AtomicFile readFrom; final String rootTag; if (!file.getBaseFile().exists()) { - if (DEBUG) Log.d(TAG, " > File does not exist -> Try to read legacy file"); - legacyBaseFile = getBaseLegacyStorageFileForUser(userId); - if (DEBUG) Log.d(TAG, " > Legacy file=" + legacyBaseFile.getPath()); if (!legacyBaseFile.exists()) { - if (DEBUG) Log.d(TAG, " > Legacy file does not exist -> Abort"); return; } @@ -277,27 +270,16 @@ final class PersistentDataStore { rootTag = XML_TAG_STATE; } - if (DEBUG) Log.d(TAG, " > Reading associations..."); final int version = readStateFromFileLocked(userId, readFrom, rootTag, associationsOut, previouslyUsedIdsPerPackageOut); - if (DEBUG) { - Log.d(TAG, " > Done reading: " + associationsOut); - if (version < CURRENT_PERSISTENCE_VERSION) { - Log.d(TAG, " > File used old format: v." + version + " -> Re-write"); - } - } if (legacyBaseFile != null || version < CURRENT_PERSISTENCE_VERSION) { // The data is either in the legacy file or in the legacy format, or both. // Save the data to right file in using the current format. - if (DEBUG) { - Log.d(TAG, " > Writing the data to " + file.getBaseFile().getPath()); - } persistStateToFileLocked(file, associationsOut, previouslyUsedIdsPerPackageOut); if (legacyBaseFile != null) { // We saved the data to the right file, can delete the old file now. - if (DEBUG) Log.d(TAG, " > Deleting legacy file"); legacyBaseFile.delete(); } } @@ -314,14 +296,12 @@ final class PersistentDataStore { * @param associations a set of user's associations. * @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user. */ - void persistStateForUser(@UserIdInt int userId, + public void persistStateForUser(@UserIdInt int userId, @NonNull Collection<AssociationInfo> associations, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { Slog.i(TAG, "Writing associations for user " + userId + " to disk"); - if (DEBUG) Slog.d(TAG, " > " + associations); final AtomicFile file = getStorageFileForUser(userId); - if (DEBUG) Log.d(TAG, " > File=" + file.getBaseFile().getPath()); // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize // accesses to the file on the file system using this AtomicFile object. synchronized (file) { @@ -404,7 +384,10 @@ final class PersistentDataStore { u -> createStorageFileForUser(userId, FILE_NAME)); } - byte[] getBackupPayload(@UserIdInt int userId) { + /** + * Get associations backup payload from disk + */ + public byte[] getBackupPayload(@UserIdInt int userId) { Slog.i(TAG, "Fetching stored state data for user " + userId + " from disk"); final AtomicFile file = getStorageFileForUser(userId); @@ -413,7 +396,10 @@ final class PersistentDataStore { } } - void readStateFromPayload(byte[] payload, @UserIdInt int userId, + /** + * Convert payload to a set of associations + */ + public void readStateFromPayload(byte[] payload, @UserIdInt int userId, @NonNull Set<AssociationInfo> associationsOut, @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { @@ -615,7 +601,7 @@ final class PersistentDataStore { macAddress, displayName, profile, null, selfManaged, notify, revoked, pending, timeApproved, lastTimeConnected, systemDataSyncFlags); } catch (Exception e) { - if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e); + Slog.e(TAG, "Could not create AssociationInfo", e); } return associationInfo; } diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index 1dab40ea5876..29ec7c2c9743 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.association; import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; import static android.app.PendingIntent.FLAG_IMMUTABLE; @@ -24,7 +24,6 @@ import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR; import static android.content.ComponentName.createRelative; import static android.content.pm.PackageManager.FEATURE_WATCH; -import static com.android.server.companion.CompanionDeviceManagerService.DEBUG; import static com.android.server.companion.utils.MetricUtils.logCreateAssociation; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PermissionsUtils.enforcePermissionForCreatingAssociation; @@ -59,6 +58,7 @@ import android.os.UserHandle; import android.util.Slog; import com.android.internal.R; +import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.utils.PackageUtils; import java.util.List; @@ -107,7 +107,7 @@ import java.util.List; * ResultReceiver, MacAddress) */ @SuppressLint("LongLogTag") -class AssociationRequestsProcessor { +public class AssociationRequestsProcessor { private static final String TAG = "CDM_AssociationRequestsProcessor"; // AssociationRequestsProcessor <-> UI @@ -130,12 +130,12 @@ class AssociationRequestsProcessor { private final @NonNull Context mContext; private final @NonNull CompanionDeviceManagerService mService; private final @NonNull PackageManagerInternal mPackageManager; - private final @NonNull AssociationStoreImpl mAssociationStore; + private final @NonNull AssociationStore mAssociationStore; @NonNull private final ComponentName mCompanionDeviceActivity; - AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service, - @NonNull AssociationStoreImpl associationStore) { + public AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service, + @NonNull AssociationStore associationStore) { mContext = service.getContext(); mService = service; mPackageManager = service.mPackageManagerInternal; @@ -149,7 +149,7 @@ class AssociationRequestsProcessor { * Handle incoming {@link AssociationRequest}s, sent via * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)} */ - void processNewAssociationRequest(@NonNull AssociationRequest request, + public void processNewAssociationRequest(@NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @NonNull IAssociationRequestCallback callback) { requireNonNull(request, "Request MUST NOT be null"); @@ -161,11 +161,8 @@ class AssociationRequestsProcessor { requireNonNull(callback, "Callback MUST NOT be null"); final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); - if (DEBUG) { - Slog.d(TAG, "processNewAssociationRequest() " - + "request=" + request + ", " - + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")"); - } + Slog.d(TAG, "processNewAssociationRequest() " + "request=" + request + ", " + "package=u" + + userId + "/" + packageName + " (uid=" + packageUid + ")"); // 1. Enforce permissions and other requirements. enforcePermissionForCreatingAssociation(mContext, request, packageUid); @@ -223,7 +220,7 @@ class AssociationRequestsProcessor { /** * Process another AssociationRequest in CompanionDeviceActivity to cancel current dialog. */ - PendingIntent buildAssociationCancellationIntent(@NonNull String packageName, + public PendingIntent buildAssociationCancellationIntent(@NonNull String packageName, @UserIdInt int userId) { requireNonNull(packageName, "Package name MUST NOT be null"); @@ -248,13 +245,6 @@ class AssociationRequestsProcessor { final int userId = request.getUserId(); final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); - if (DEBUG) { - Slog.d(TAG, "processAssociationRequestApproval()\n" - + " package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n" - + " request=" + request + "\n" - + " macAddress=" + macAddress + "\n"); - } - // 1. Need to check permissions again in case something changed, since we first received // this request. try { @@ -288,6 +278,9 @@ class AssociationRequestsProcessor { } } + /** + * Create an association. + */ public void createAssociation(@UserIdInt int userId, @NonNull String packageName, @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, @@ -309,6 +302,9 @@ class AssociationRequestsProcessor { // that there are other devices with the same profile, so the role holder won't be removed. } + /** + * Grant a role if specified and add an association to store. + */ public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver) { @@ -331,6 +327,9 @@ class AssociationRequestsProcessor { }); } + /** + * Enable system data sync. + */ public void enableSystemDataSync(int associationId, int flags) { AssociationInfo association = mAssociationStore.getAssociationById(associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) @@ -338,6 +337,9 @@ class AssociationRequestsProcessor { mAssociationStore.updateAssociation(updated); } + /** + * Disable system data sync. + */ public void disableSystemDataSync(int associationId, int flags) { AssociationInfo association = mAssociationStore.getAssociationById(associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) @@ -350,7 +352,8 @@ class AssociationRequestsProcessor { mAssociationStore.addAssociation(association); - mService.updateSpecialAccessPermissionForAssociatedPackage(association); + mService.updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), + association.getPackageName()); logCreateAssociation(association.getDeviceProfile()); } @@ -431,38 +434,37 @@ class AssociationRequestsProcessor { private final ResultReceiver mOnRequestConfirmationReceiver = new ResultReceiver(Handler.getMain()) { - @Override - protected void onReceiveResult(int resultCode, Bundle data) { - if (DEBUG) { - Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() " - + "code=" + resultCode + ", " + "data=" + data); - } - - if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) { - Slog.w(TAG, "Unknown result code:" + resultCode); - return; - } - - final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST, android.companion.AssociationRequest.class); - final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub - .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK)); - final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER, android.os.ResultReceiver.class); - - requireNonNull(request); - requireNonNull(callback); - requireNonNull(resultReceiver); - - final MacAddress macAddress; - if (request.isSelfManaged()) { - macAddress = null; - } else { - macAddress = data.getParcelable(EXTRA_MAC_ADDRESS, android.net.MacAddress.class); - requireNonNull(macAddress); - } - - processAssociationRequestApproval(request, callback, resultReceiver, macAddress); - } - }; + @Override + protected void onReceiveResult(int resultCode, Bundle data) { + if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) { + Slog.w(TAG, "Unknown result code:" + resultCode); + return; + } + + final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST, + android.companion.AssociationRequest.class); + final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub + .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK)); + final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER, + android.os.ResultReceiver.class); + + requireNonNull(request); + requireNonNull(callback); + requireNonNull(resultReceiver); + + final MacAddress macAddress; + if (request.isSelfManaged()) { + macAddress = null; + } else { + macAddress = data.getParcelable(EXTRA_MAC_ADDRESS, + android.net.MacAddress.class); + requireNonNull(macAddress); + } + + processAssociationRequestApproval(request, callback, resultReceiver, + macAddress); + } + }; private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) { // Throttle frequent associations diff --git a/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java index 10963ea37e8e..490be0da593b 100644 --- a/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.association; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; @@ -38,6 +38,8 @@ import android.util.Log; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.server.companion.CompanionApplicationController; +import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; @@ -55,7 +57,7 @@ public class AssociationRevokeProcessor { private static final boolean DEBUG = false; private final @NonNull Context mContext; private final @NonNull CompanionDeviceManagerService mService; - private final @NonNull AssociationStoreImpl mAssociationStore; + private final @NonNull AssociationStore mAssociationStore; private final @NonNull PackageManagerInternal mPackageManagerInternal; private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor; private final @NonNull SystemDataTransferRequestStore mSystemDataTransferRequestStore; @@ -90,8 +92,8 @@ public class AssociationRevokeProcessor { @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>(); - AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service, - @NonNull AssociationStoreImpl associationStore, + public AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service, + @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, @NonNull CompanionApplicationController applicationController, @@ -108,8 +110,11 @@ public class AssociationRevokeProcessor { mSystemDataTransferRequestStore = systemDataTransferRequestStore; } + /** + * Disassociate an association + */ // TODO: also revoke notification access - void disassociateInternal(int associationId) { + public void disassociateInternal(int associationId) { final AssociationInfo association = mAssociationStore.getAssociationById(associationId); final int userId = association.getUserId(); final String packageName = association.getPackageName(); @@ -168,7 +173,7 @@ public class AssociationRevokeProcessor { * {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process, * which would lead to the poor UX, hence need to try later. */ - boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) { + public boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) { if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association); final String deviceProfile = association.getDeviceProfile(); @@ -208,15 +213,6 @@ public class AssociationRevokeProcessor { return true; } - @SuppressLint("MissingPermission") - private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { - return Binder.withCleanCallingIdentity(() -> { - final int uid = - mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); - return mActivityManager.getUidImportance(uid); - }); - } - /** * Set revoked flag for active association and add the revoked association and the uid into * the caches. @@ -225,7 +221,7 @@ public class AssociationRevokeProcessor { * @see #mUidsPendingRoleHolderRemoval * @see OnPackageVisibilityChangeListener */ - void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + public void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) { // First: set revoked flag association = (new AssociationInfo.Builder(association)).setRevoked(true).build(); final String packageName = association.getPackageName(); @@ -247,6 +243,28 @@ public class AssociationRevokeProcessor { } /** + * @return a copy of the revoked associations set (safeguarding against + * {@code ConcurrentModificationException}-s). + */ + @NonNull + public Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser( + @UserIdInt int userId) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + // Return a copy. + return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)); + } + } + + @SuppressLint("MissingPermission") + private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { + return Binder.withCleanCallingIdentity(() -> { + final int uid = + mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + return mActivityManager.getUidImportance(uid); + }); + } + + /** * Remove the revoked association from the cache and also remove the uid from the map if * there are other associations with the same package still pending for role holder removal. * @@ -279,18 +297,6 @@ public class AssociationRevokeProcessor { } } - /** - * @return a copy of the revoked associations set (safeguarding against - * {@code ConcurrentModificationException}-s). - */ - @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser( - @UserIdInt int userId) { - synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { - // Return a copy. - return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)); - } - } - private String getPackageNameByUid(int uid) { synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { return mUidsPendingRoleHolderRemoval.get(uid); diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index 8c6ad3bad857..2f94bdebb988 100644 --- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.association; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.net.MacAddress; -import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -30,6 +30,8 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -40,24 +42,69 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.StringJoiner; /** - * Implementation of the {@link AssociationStore}, with addition of the methods for modification. - * <ul> - * <li> {@link #addAssociation(AssociationInfo)} - * <li> {@link #removeAssociation(int)} - * <li> {@link #updateAssociation(AssociationInfo)} - * </ul> - * - * The class has package-private access level, and instances of the class should only be created by - * the {@link CompanionDeviceManagerService}. - * Other system component (both inside and outside if the com.android.server.companion package) - * should use public {@link AssociationStore} interface. + * Association store for CRUD. */ @SuppressLint("LongLogTag") -class AssociationStoreImpl implements AssociationStore { - private static final boolean DEBUG = false; +public class AssociationStore { + + @IntDef(prefix = { "CHANGE_TYPE_" }, value = { + CHANGE_TYPE_ADDED, + CHANGE_TYPE_REMOVED, + CHANGE_TYPE_UPDATED_ADDRESS_CHANGED, + CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ChangeType {} + + public static final int CHANGE_TYPE_ADDED = 0; + public static final int CHANGE_TYPE_REMOVED = 1; + public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2; + public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3; + + /** Listener for any changes to associations. */ + public interface OnChangeListener { + /** + * Called when there are association changes. + */ + default void onAssociationChanged( + @AssociationStore.ChangeType int changeType, AssociationInfo association) { + switch (changeType) { + case CHANGE_TYPE_ADDED: + onAssociationAdded(association); + break; + + case CHANGE_TYPE_REMOVED: + onAssociationRemoved(association); + break; + + case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED: + onAssociationUpdated(association, true); + break; + + case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED: + onAssociationUpdated(association, false); + break; + } + } + + /** + * Called when an association is added. + */ + default void onAssociationAdded(AssociationInfo association) {} + + /** + * Called when an association is removed. + */ + default void onAssociationRemoved(AssociationInfo association) {} + + /** + * Called when an association is updated. + */ + default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {} + } + private static final String TAG = "CDM_AssociationStore"; private final Object mLock = new Object(); @@ -72,17 +119,17 @@ class AssociationStoreImpl implements AssociationStore { @GuardedBy("mListeners") private final Set<OnChangeListener> mListeners = new LinkedHashSet<>(); - void addAssociation(@NonNull AssociationInfo association) { + /** + * Add an association. + */ + public void addAssociation(@NonNull AssociationInfo association) { + Slog.i(TAG, "Adding new association=" + association); + // Validity check first. checkNotRevoked(association); final int id = association.getId(); - if (DEBUG) { - Log.i(TAG, "addAssociation() " + association.toShortString()); - Log.d(TAG, " association=" + association); - } - synchronized (mLock) { if (mIdMap.containsKey(id)) { Slog.e(TAG, "Association with id " + id + " already exists."); @@ -96,34 +143,34 @@ class AssociationStoreImpl implements AssociationStore { } invalidateCacheForUserLocked(association.getUserId()); + + Slog.i(TAG, "Done adding new association."); } broadcastChange(CHANGE_TYPE_ADDED, association); } - void updateAssociation(@NonNull AssociationInfo updated) { + /** + * Update an association. + */ + public void updateAssociation(@NonNull AssociationInfo updated) { + Slog.i(TAG, "Updating new association=" + updated); // Validity check first. checkNotRevoked(updated); final int id = updated.getId(); - if (DEBUG) { - Log.i(TAG, "updateAssociation() " + updated.toShortString()); - Log.d(TAG, " updated=" + updated); - } - final AssociationInfo current; final boolean macAddressChanged; synchronized (mLock) { current = mIdMap.get(id); if (current == null) { - if (DEBUG) Log.w(TAG, "Association with id " + id + " does not exist."); + Slog.w(TAG, "Can't update association. It does not exist."); return; } - if (DEBUG) Log.d(TAG, " current=" + current); if (current.equals(updated)) { - if (DEBUG) Log.w(TAG, " No changes."); + Slog.w(TAG, "Association is the same."); return; } @@ -144,6 +191,7 @@ class AssociationStoreImpl implements AssociationStore { mAddressMap.computeIfAbsent(updatedAddress, it -> new HashSet<>()).add(id); } } + Slog.i(TAG, "Done updating association."); } final int changeType = macAddressChanged ? CHANGE_TYPE_UPDATED_ADDRESS_CHANGED @@ -151,21 +199,19 @@ class AssociationStoreImpl implements AssociationStore { broadcastChange(changeType, updated); } - void removeAssociation(int id) { - if (DEBUG) Log.i(TAG, "removeAssociation() id=" + id); + /** + * Remove an association + */ + public void removeAssociation(int id) { + Slog.i(TAG, "Removing association id=" + id); final AssociationInfo association; synchronized (mLock) { association = mIdMap.remove(id); if (association == null) { - if (DEBUG) Log.w(TAG, "Association with id " + id + " is not stored."); + Slog.w(TAG, "Can't remove association. It does not exist."); return; - } else { - if (DEBUG) { - Log.i(TAG, "removed " + association.toShortString()); - Log.d(TAG, " association=" + association); - } } final MacAddress macAddress = association.getDeviceMacAddress(); @@ -174,6 +220,8 @@ class AssociationStoreImpl implements AssociationStore { } invalidateCacheForUserLocked(association.getUserId()); + + Slog.i(TAG, "Done removing association."); } broadcastChange(CHANGE_TYPE_REMOVED, association); @@ -195,12 +243,18 @@ class AssociationStoreImpl implements AssociationStore { } } + /** + * Get associations for the user. + */ public @NonNull List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId) { synchronized (mLock) { return getAssociationsForUserLocked(userId); } } + /** + * Get associations for the package + */ public @NonNull List<AssociationInfo> getAssociationsForPackage( @UserIdInt int userId, @NonNull String packageName) { final List<AssociationInfo> associationsForUser = getAssociationsForUser(userId); @@ -210,6 +264,9 @@ class AssociationStoreImpl implements AssociationStore { return Collections.unmodifiableList(associationsForPackage); } + /** + * Get associations by mac address for the package. + */ public @Nullable AssociationInfo getAssociationsForPackageWithAddress( @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) { final List<AssociationInfo> associations = getAssociationsByAddress(macAddress); @@ -217,13 +274,20 @@ class AssociationStoreImpl implements AssociationStore { it -> it.belongsToPackage(userId, packageName)); } + /** + * Get association by id. + */ public @Nullable AssociationInfo getAssociationById(int id) { synchronized (mLock) { return mIdMap.get(id); } } - public @NonNull List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) { + /** + * Get associations by mac address. + */ + @NonNull + public List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) { final MacAddress address = MacAddress.fromString(macAddress); synchronized (mLock) { @@ -240,7 +304,8 @@ class AssociationStoreImpl implements AssociationStore { } @GuardedBy("mLock") - private @NonNull List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) { + @NonNull + private List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) { final List<AssociationInfo> cached = mCachedPerUser.get(userId); if (cached != null) { return cached; @@ -262,12 +327,18 @@ class AssociationStoreImpl implements AssociationStore { mCachedPerUser.delete(userId); } + /** + * Register a listener for association changes. + */ public void registerListener(@NonNull OnChangeListener listener) { synchronized (mListeners) { mListeners.add(listener); } } + /** + * Unregister a listener previously registered for association changes. + */ public void unregisterListener(@NonNull OnChangeListener listener) { synchronized (mListeners) { mListeners.remove(listener); @@ -297,43 +368,30 @@ class AssociationStoreImpl implements AssociationStore { } } - void setAssociations(Collection<AssociationInfo> allAssociations) { + /** + * Set associations to cache. It will clear the existing cache. + */ + public void setAssociationsToCache(Collection<AssociationInfo> associations) { // Validity check first. - allAssociations.forEach(AssociationStoreImpl::checkNotRevoked); + associations.forEach(AssociationStore::checkNotRevoked); - if (DEBUG) { - Log.i(TAG, "setAssociations() n=" + allAssociations.size()); - final StringJoiner stringJoiner = new StringJoiner(", "); - allAssociations.forEach(assoc -> stringJoiner.add(assoc.toShortString())); - Log.v(TAG, " associations=" + stringJoiner); - } synchronized (mLock) { - setAssociationsLocked(allAssociations); - } - } - - @GuardedBy("mLock") - private void setAssociationsLocked(Collection<AssociationInfo> associations) { - clearLocked(); + mIdMap.clear(); + mAddressMap.clear(); + mCachedPerUser.clear(); - for (AssociationInfo association : associations) { - final int id = association.getId(); - mIdMap.put(id, association); + for (AssociationInfo association : associations) { + final int id = association.getId(); + mIdMap.put(id, association); - final MacAddress address = association.getDeviceMacAddress(); - if (address != null) { - mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id); + final MacAddress address = association.getDeviceMacAddress(); + if (address != null) { + mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id); + } } } } - @GuardedBy("mLock") - private void clearLocked() { - mIdMap.clear(); - mAddressMap.clear(); - mCachedPerUser.clear(); - } - private static void checkNotRevoked(@NonNull AssociationInfo association) { if (association.isRevoked()) { throw new IllegalArgumentException( diff --git a/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java index aac628cab403..894c49a2b5cf 100644 --- a/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; - -import static com.android.server.companion.CompanionDeviceManagerService.TAG; +package com.android.server.companion.association; import static java.util.concurrent.TimeUnit.DAYS; @@ -29,13 +27,17 @@ import android.content.Context; import android.util.Slog; import com.android.server.LocalServices; +import com.android.server.companion.CompanionDeviceManagerServiceInternal; /** - * A Job Service responsible for clean up the Association. + * A Job Service responsible for clean up idle self-managed associations. + * * The job will be executed only if the device is charging and in idle mode due to the application - * will be killed if association/role are revoked. + * will be killed if association/role are revoked. See {@link AssociationRevokeProcessor} */ public class InactiveAssociationsRemovalService extends JobService { + + private static final String TAG = "CDM_InactiveAssociationsRemovalService"; private static final String JOB_NAMESPACE = "companion"; private static final int JOB_ID = 1; private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1); @@ -60,7 +62,10 @@ public class InactiveAssociationsRemovalService extends JobService { return false; } - static void schedule(Context context) { + /** + * Schedule this job. + */ + public static void schedule(Context context) { Slog.i(TAG, "Scheduling the Association Removal job"); final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class).forNamespace(JOB_NAMESPACE); diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index 74236a402244..a08e0da90d49 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -52,8 +52,8 @@ import android.permission.PermissionControllerManager; import android.util.Slog; import com.android.internal.R; -import com.android.server.companion.AssociationStore; import com.android.server.companion.CompanionDeviceManagerService; +import com.android.server.companion.association.AssociationStore; import com.android.server.companion.transport.CompanionTransportManager; import com.android.server.companion.utils.PackageUtils; import com.android.server.companion.utils.PermissionsUtils; diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java index 2899c055afbd..99466a966647 100644 --- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java +++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java @@ -59,8 +59,8 @@ import android.os.Looper; import android.util.Log; import android.util.Slog; -import com.android.server.companion.AssociationStore; -import com.android.server.companion.AssociationStore.ChangeType; +import com.android.server.companion.association.AssociationStore; +import com.android.server.companion.association.AssociationStore.ChangeType; import java.util.ArrayList; import java.util.Arrays; diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index 0287f6258c06..4da3f9bead4e 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -39,7 +39,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; -import com.android.server.companion.AssociationStore; +import com.android.server.companion.association.AssociationStore; import java.util.Arrays; import java.util.Collections; diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java index 3da9693b75c9..37bbb937d1b5 100644 --- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java +++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java @@ -44,7 +44,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import com.android.internal.annotations.GuardedBy; -import com.android.server.companion.AssociationStore; +import com.android.server.companion.association.AssociationStore; import java.io.PrintWriter; import java.util.HashSet; diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 3861f99eb03c..6dd14ac91a34 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -32,7 +32,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; -import com.android.server.companion.AssociationStore; +import com.android.server.companion.association.AssociationStore; import java.io.FileDescriptor; import java.io.IOException; diff --git a/services/companion/java/com/android/server/companion/utils/AssociationUtils.java b/services/companion/java/com/android/server/companion/utils/AssociationUtils.java new file mode 100644 index 000000000000..e4d96413cf8b --- /dev/null +++ b/services/companion/java/com/android/server/companion/utils/AssociationUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.utils; + +import android.annotation.UserIdInt; + +public final class AssociationUtils { + + /** Range of Association IDs allocated for a user. */ + private static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000; + + /** + * Get the left boundary of the association id range for the user. + */ + public static int getFirstAssociationIdForUser(@UserIdInt int userId) { + // We want the IDs to start from 1, not 0. + return userId * ASSOCIATIONS_IDS_PER_USER_RANGE + 1; + } + + /** + * Get the right boundary of the association id range for the user. + */ + public static int getLastAssociationIdForUser(@UserIdInt int userId) { + return (userId + 1) * ASSOCIATIONS_IDS_PER_USER_RANGE; + } + + private AssociationUtils() {} +} diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 6d731b21ac8a..8d93b048e588 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -210,7 +210,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mActivityListener.onTopActivityChanged(displayId, topActivity, UserHandle.USER_NULL); } catch (RemoteException e) { - Slog.w(TAG, "Unable to call mActivityListener", e); + Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); } } @@ -220,7 +220,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub try { mActivityListener.onTopActivityChanged(displayId, topActivity, userId); } catch (RemoteException e) { - Slog.w(TAG, "Unable to call mActivityListener", e); + Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); } } @@ -229,7 +229,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub try { mActivityListener.onDisplayEmpty(displayId); } catch (RemoteException e) { - Slog.w(TAG, "Unable to call mActivityListener", e); + Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); } } }; diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index e1d7be121865..1a3ef73ca2a8 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -264,8 +264,8 @@ public class SystemConfig { final ArrayMap<String, ArraySet<String>> mAllowIgnoreLocationSettings = new ArrayMap<>(); // These are the packages that are allow-listed to be able to access camera when - // the camera privacy state is for driver assistance apps only. - final ArrayMap<String, Boolean> mAllowlistCameraPrivacy = new ArrayMap<>(); + // the camera privacy state is enabled. + final ArraySet<String> mAllowlistCameraPrivacy = new ArraySet<>(); // These are the action strings of broadcasts which are whitelisted to // be delivered anonymously even to apps which target O+. @@ -489,7 +489,7 @@ public class SystemConfig { return mAllowedAssociations; } - public ArrayMap<String, Boolean> getCameraPrivacyAllowlist() { + public ArraySet<String> getCameraPrivacyAllowlist() { return mAllowlistCameraPrivacy; } @@ -1076,13 +1076,11 @@ public class SystemConfig { case "camera-privacy-allowlisted-app" : { if (allowOverrideAppRestrictions) { String pkgname = parser.getAttributeValue(null, "package"); - boolean isMandatory = XmlUtils.readBooleanAttribute( - parser, "mandatory", false); if (pkgname == null) { Slog.w(TAG, "<" + name + "> without package in " + permFile + " at " + parser.getPositionDescription()); } else { - mAllowlistCameraPrivacy.put(pkgname, isMandatory); + mAllowlistCameraPrivacy.add(pkgname); } } else { logNotAllowedInPartition(name, permFile, parser); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 663ba8a38d77..cfe1e181871d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4779,36 +4779,47 @@ public class ActivityManagerService extends IActivityManager.Stub // being bound to an application. thread.runIsolatedEntryPoint( app.getIsolatedEntryPoint(), app.getIsolatedEntryPointArgs()); - } else if (instr2 != null) { - thread.bindApplication(processName, appInfo, - app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage, - instr2.mIsSdkInSandbox, - providerList, - instr2.mClass, - profilerInfo, instr2.mArguments, - instr2.mWatcher, - instr2.mUiAutomationConnection, testMode, - mBinderTransactionTrackingEnabled, enableTrackAllocation, - isRestrictedBackupMode || !normalMode, app.isPersistent(), - new Configuration(app.getWindowProcessController().getConfiguration()), - app.getCompat(), getCommonServicesLocked(app.isolated), - mCoreSettingsObserver.getCoreSettingsLocked(), - buildSerial, autofillOptions, contentCaptureOptions, - app.getDisabledCompatChanges(), serializedSystemFontMap, - app.getStartElapsedTime(), app.getStartUptime()); } else { - thread.bindApplication(processName, appInfo, - app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage, - /* isSdkInSandbox= */ false, - providerList, null, profilerInfo, null, null, null, testMode, - mBinderTransactionTrackingEnabled, enableTrackAllocation, - isRestrictedBackupMode || !normalMode, app.isPersistent(), + boolean isSdkInSandbox = false; + ComponentName instrumentationName = null; + Bundle instrumentationArgs = null; + IInstrumentationWatcher instrumentationWatcher = null; + IUiAutomationConnection instrumentationUiConnection = null; + if (instr2 != null) { + isSdkInSandbox = instr2.mIsSdkInSandbox; + instrumentationName = instr2.mClass; + instrumentationArgs = instr2.mArguments; + instrumentationWatcher = instr2.mWatcher; + instrumentationUiConnection = instr2.mUiAutomationConnection; + } + thread.bindApplication( + processName, + appInfo, + app.sdkSandboxClientAppVolumeUuid, + app.sdkSandboxClientAppPackage, + isSdkInSandbox, + providerList, + instrumentationName, + profilerInfo, + instrumentationArgs, + instrumentationWatcher, + instrumentationUiConnection, + testMode, + mBinderTransactionTrackingEnabled, + enableTrackAllocation, + isRestrictedBackupMode || !normalMode, + app.isPersistent(), new Configuration(app.getWindowProcessController().getConfiguration()), - app.getCompat(), getCommonServicesLocked(app.isolated), + app.getCompat(), + getCommonServicesLocked(app.isolated), mCoreSettingsObserver.getCoreSettingsLocked(), - buildSerial, autofillOptions, contentCaptureOptions, - app.getDisabledCompatChanges(), serializedSystemFontMap, - app.getStartElapsedTime(), app.getStartUptime()); + buildSerial, + autofillOptions, + contentCaptureOptions, + app.getDisabledCompatChanges(), + serializedSystemFontMap, + app.getStartElapsedTime(), + app.getStartUptime()); } Message msg = mHandler.obtainMessage(BIND_APPLICATION_TIMEOUT_SOFT_MSG); @@ -9363,7 +9374,9 @@ public class ActivityManagerService extends IActivityManager.Stub sb.append("Animations-Running: ").append(info.numAnimationsRunning).append("\n"); } if (info.broadcastIntentAction != null) { - sb.append("Broadcast-Intent-Action: ").append(info.broadcastIntentAction).append("\n"); + sb.append("Broadcast-Intent-Action: ") + .append(info.broadcastIntentAction) + .append("\n"); } if (info.durationMillis != -1) { sb.append("Duration-Millis: ").append(info.durationMillis).append("\n"); @@ -9723,82 +9736,126 @@ public class ActivityManagerService extends IActivityManager.Stub // If process is null, we are being called from some internal code // and may be about to die -- run this synchronously. final boolean runSynchronously = process == null; - Thread worker = new Thread("Error dump: " + dropboxTag) { - @Override - public void run() { - if (report != null) { - sb.append(report); - } - - String logcatSetting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag; - String maxBytesSetting = Settings.Global.MAX_ERROR_BYTES_PREFIX + dropboxTag; - int lines = Build.IS_USER - ? 0 - : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0); - int dropboxMaxSize = Settings.Global.getInt( - mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE); - - if (dataFile != null) { - // Attach the stack traces file to the report so collectors can load them - // by file if they have access. - sb.append(DATA_FILE_PATH_HEADER) - .append(dataFile.getAbsolutePath()).append('\n'); - - int maxDataFileSize = dropboxMaxSize - - sb.length() - - lines * RESERVED_BYTES_PER_LOGCAT_LINE - - DATA_FILE_PATH_FOOTER.length(); - - if (maxDataFileSize > 0) { - // Inline dataFile contents if there is room. - try { - sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize, - "\n\n[[TRUNCATED]]\n")); - } catch (IOException e) { - Slog.e(TAG, "Error reading " + dataFile, e); + Thread worker = + new Thread("Error dump: " + dropboxTag) { + @Override + public void run() { + if (report != null) { + sb.append(report); } - } - // Always append the footer, even there wasn't enough space to inline the - // dataFile contents. - sb.append(DATA_FILE_PATH_FOOTER); - } + String logcatSetting = Settings.Global.ERROR_LOGCAT_PREFIX + dropboxTag; + String maxBytesSetting = + Settings.Global.MAX_ERROR_BYTES_PREFIX + dropboxTag; + int lines = + Build.IS_USER + ? 0 + : Settings.Global.getInt( + mContext.getContentResolver(), logcatSetting, 0); + int dropboxMaxSize = + Settings.Global.getInt( + mContext.getContentResolver(), + maxBytesSetting, + DROPBOX_DEFAULT_MAX_SIZE); + + if (dataFile != null) { + // Attach the stack traces file to the report so collectors can load + // them + // by file if they have access. + sb.append(DATA_FILE_PATH_HEADER) + .append(dataFile.getAbsolutePath()) + .append('\n'); + + int maxDataFileSize = + dropboxMaxSize + - sb.length() + - lines * RESERVED_BYTES_PER_LOGCAT_LINE + - DATA_FILE_PATH_FOOTER.length(); + + if (maxDataFileSize > 0) { + // Inline dataFile contents if there is room. + try { + sb.append( + FileUtils.readTextFile( + dataFile, + maxDataFileSize, + "\n\n[[TRUNCATED]]\n")); + } catch (IOException e) { + Slog.e(TAG, "Error reading " + dataFile, e); + } + } - if (crashInfo != null && crashInfo.stackTrace != null) { - sb.append(crashInfo.stackTrace); - } + // Always append the footer, even there wasn't enough space to inline + // the + // dataFile contents. + sb.append(DATA_FILE_PATH_FOOTER); + } - if (lines > 0 && !runSynchronously) { - sb.append("\n"); + if (crashInfo != null && crashInfo.stackTrace != null) { + sb.append(crashInfo.stackTrace); + } - InputStreamReader input = null; - try { - java.lang.Process logcat = new ProcessBuilder( - // Time out after 10s of inactivity, but kill logcat with SEGV - // so we can investigate why it didn't finish. - "/system/bin/timeout", "-i", "-s", "SEGV", "10s", - // Merge several logcat streams, and take the last N lines. - "/system/bin/logcat", "-v", "threadtime", "-b", "events", "-b", "system", - "-b", "main", "-b", "crash", "-t", String.valueOf(lines)) - .redirectErrorStream(true).start(); - - try { logcat.getOutputStream().close(); } catch (IOException e) {} - try { logcat.getErrorStream().close(); } catch (IOException e) {} - input = new InputStreamReader(logcat.getInputStream()); - - int num; - char[] buf = new char[8192]; - while ((num = input.read(buf)) > 0) sb.append(buf, 0, num); - } catch (IOException e) { - Slog.e(TAG, "Error running logcat", e); - } finally { - if (input != null) try { input.close(); } catch (IOException e) {} - } - } + if (lines > 0 && !runSynchronously) { + sb.append("\n"); - dbox.addText(dropboxTag, sb.toString()); - } - }; + InputStreamReader input = null; + try { + java.lang.Process logcat = + new ProcessBuilder( + // Time out after 10s of inactivity, but + // kill logcat with SEGV + // so we can investigate why it didn't + // finish. + "/system/bin/timeout", + "-i", + "-s", + "SEGV", + "10s", + // Merge several logcat streams, and take + // the last N lines. + "/system/bin/logcat", + "-v", + "threadtime", + "-b", + "events", + "-b", + "system", + "-b", + "main", + "-b", + "crash", + "-t", + String.valueOf(lines)) + .redirectErrorStream(true) + .start(); + + try { + logcat.getOutputStream().close(); + } catch (IOException e) { + } + try { + logcat.getErrorStream().close(); + } catch (IOException e) { + } + input = new InputStreamReader(logcat.getInputStream()); + + int num; + char[] buf = new char[8192]; + while ((num = input.read(buf)) > 0) sb.append(buf, 0, num); + } catch (IOException e) { + Slog.e(TAG, "Error running logcat", e); + } finally { + if (input != null) + try { + input.close(); + } catch (IOException e) { + } + } + } + + dbox.addText(dropboxTag, sb.toString()); + } + }; if (runSynchronously) { final int oldMask = StrictMode.allowThreadDiskWritesMask(); @@ -10166,43 +10223,48 @@ public class ActivityManagerService extends IActivityManager.Stub mOomAdjuster.dumpCacheOomRankerSettings(pw); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); - + pw.println( + "-------------------------------------------------------------------------------"); } dumpAllowedAssociationsLocked(fd, pw, args, opti, dumpAll, dumpPackage); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); - + pw.println( + "-------------------------------------------------------------------------------"); } mPendingIntentController.dumpPendingIntents(pw, dumpAll, dumpPackage); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } dumpBroadcastsLocked(fd, pw, args, opti, dumpAll, dumpPackage); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } if (dumpAll || dumpPackage != null) { dumpBroadcastStatsLocked(fd, pw, args, opti, dumpAll, dumpPackage); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } } mCpHelper.dumpProvidersLocked(fd, pw, args, opti, dumpAll, dumpPackage); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } dumpPermissions(fd, pw, args, opti, dumpAll, dumpPackage); pw.println(); sdumper = mServices.newServiceDumperLocked(fd, pw, args, opti, dumpAll, dumpPackage); if (!dumpClient) { if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } sdumper.dumpLocked(); } @@ -10217,7 +10279,8 @@ public class ActivityManagerService extends IActivityManager.Stub // method with the lock held. if (dumpClient) { if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } sdumper.dumpWithClient(); } @@ -10230,33 +10293,38 @@ public class ActivityManagerService extends IActivityManager.Stub // proxies in the first place. pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } dumpBinderProxies(pw, BINDER_PROXY_HIGH_WATERMARK /* minToDump */); } synchronized(this) { pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mAtmInternal.dump(DUMP_RECENTS_CMD, fd, pw, args, opti, dumpAll, dumpClient, dumpPackage, displayIdFilter); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mAtmInternal.dump(DUMP_LASTANR_CMD, fd, pw, args, opti, dumpAll, dumpClient, dumpPackage, displayIdFilter); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mAtmInternal.dump(DUMP_STARTER_CMD, fd, pw, args, opti, dumpAll, dumpClient, dumpPackage, displayIdFilter); if (dumpPackage == null) { pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mAtmInternal.dump(DUMP_CONTAINERS_CMD, fd, pw, args, opti, dumpAll, dumpClient, dumpPackage, displayIdFilter); @@ -10266,7 +10334,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (!dumpNormalPriority) { pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mAtmInternal.dump(DUMP_ACTIVITIES_CMD, fd, pw, args, opti, dumpAll, dumpClient, dumpPackage, displayIdFilter); @@ -10274,45 +10343,53 @@ public class ActivityManagerService extends IActivityManager.Stub if (mAssociations.size() > 0) { pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } dumpAssociationsLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage); } pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); mProcessList.getAppStartInfoTracker().dumpHistoryProcessStartInfo(pw, dumpPackage); - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage); } if (dumpPackage == null) { pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mOomAdjProfiler.dump(pw); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } dumpLmkLocked(pw); } pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } synchronized (mProcLock) { mProcessList.dumpProcessesLSP(fd, pw, args, opti, dumpAll, dumpPackage, dumpAppId); } pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } dumpUsers(pw); pw.println(); if (dumpAll) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); } mComponentAliasResolver.dump(pw); } @@ -10322,7 +10399,8 @@ public class ActivityManagerService extends IActivityManager.Stub * Dump the app restriction controller, it's required not to hold the global lock here. */ private void dumpAppRestrictionController(PrintWriter pw) { - pw.println("-------------------------------------------------------------------------------"); + pw.println( + "-------------------------------------------------------------------------------"); mAppRestrictionController.dump(pw, ""); } @@ -11462,7 +11540,9 @@ public class ActivityManagerService extends IActivityManager.Stub void dumpAllowedAssociationsLocked(FileDescriptor fd, PrintWriter pw, String[] args, int opti, boolean dumpAll, String dumpPackage) { - pw.println("ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity allowed-associations)"); + pw.println( + "ACTIVITY MANAGER ALLOWED ASSOCIATION STATE (dumpsys activity" + + " allowed-associations)"); boolean printed = false; if (mAllowedAssociations != null) { for (int i = 0; i < mAllowedAssociations.size(); i++) { @@ -12816,7 +12896,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (!opts.isCompact) { pw.print(" Used RAM: "); pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss + kernelUsed)); pw.print(" ("); - pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss)); pw.print(" used pss + "); + pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss)); + pw.print(" used pss + "); pw.print(stringifyKBSize(kernelUsed)); pw.print(" kernel)\n"); pw.print(" Lost RAM: "); pw.println(stringifyKBSize(lostRAM)); } else { @@ -13686,8 +13767,15 @@ public class ActivityManagerService extends IActivityManager.Stub } validateServiceInstanceName(instanceName); - if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, - "*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground); + if (DEBUG_SERVICE) + Slog.v( + TAG_SERVICE, + "*** startService: " + + service + + " type=" + + resolvedType + + " fg=" + + requireForeground); final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); @@ -15448,9 +15536,14 @@ public class ActivityManagerService extends IActivityManager.Stub if (checkPermission(android.Manifest.permission.BROADCAST_STICKY, callingPid, callingUid) != PackageManager.PERMISSION_GRANTED) { - String msg = "Permission Denial: broadcastIntent() requesting a sticky broadcast from pid=" - + callingPid + ", uid=" + callingUid - + " requires " + android.Manifest.permission.BROADCAST_STICKY; + String msg = + "Permission Denial: broadcastIntent() requesting a sticky broadcast from" + + " pid=" + + callingPid + + ", uid=" + + callingUid + + " requires " + + android.Manifest.permission.BROADCAST_STICKY; Slog.w(TAG, msg); throw new SecurityException(msg); } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index d1c8c303524b..4a3791396828 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -276,6 +276,11 @@ class BroadcastProcessQueue { && record.getDeliveryGroupPolicy() == BroadcastOptions.DELIVERY_GROUP_POLICY_ALL) { final BroadcastRecord replacedBroadcastRecord = replaceBroadcast(record, recordIndex); if (replacedBroadcastRecord != null) { + if (mLastDeferredStates && shouldBeDeferred() + && (record.getDeliveryState(recordIndex) + == BroadcastRecord.DELIVERY_PENDING)) { + deferredStatesApplyConsumer.accept(record, recordIndex); + } return replacedBroadcastRecord; } } diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java index d584c99cea72..d4e46a930124 100644 --- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java +++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java @@ -179,15 +179,18 @@ final class ALSProbe implements Probe { nextConsumer.consume(current); } else if (mNextConsumer != null) { mNextConsumer.add(nextConsumer); - } else { + } else if (mLightSensor != null) { mDestroyed = false; mNextConsumer = nextConsumer; enableLightSensorLoggingLocked(); + } else { + Slog.w(TAG, "No light sensor - use current to consume"); + nextConsumer.consume(current); } } private void enableLightSensorLoggingLocked() { - if (!mEnabled) { + if (!mEnabled && mLightSensor != null) { mEnabled = true; mLastAmbientLux = -1; mSensorManager.registerListener(mLightSensorListener, mLightSensor, @@ -201,7 +204,7 @@ final class ALSProbe implements Probe { private void disableLightSensorLoggingLocked(boolean destroying) { resetTimerLocked(false /* start */); - if (mEnabled) { + if (mEnabled && mLightSensor != null) { mEnabled = false; if (!destroying) { mLastAmbientLux = -1; diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java index a43f93a9beac..91e560e19f0d 100644 --- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java +++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java @@ -20,7 +20,6 @@ import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIG import android.annotation.Nullable; import android.hardware.display.DisplayManagerInternal; -import android.os.PowerManager; import android.os.Trace; /** @@ -110,7 +109,6 @@ public class DisplayOffloadSessionImpl implements DisplayManagerInternal.Display try { mDisplayOffloader.stopOffload(); mIsActive = false; - mDisplayPowerController.setBrightnessFromOffload(PowerManager.BRIGHTNESS_INVALID_FLOAT); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 2010aca72494..7f014f6dae28 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -375,7 +375,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // information. // At the time of this writing, this value is changed within updatePowerState() only, which is // limited to the thread used by DisplayControllerHandler. - private final BrightnessReason mBrightnessReason = new BrightnessReason(); + @VisibleForTesting + final BrightnessReason mBrightnessReason = new BrightnessReason(); private final BrightnessReason mBrightnessReasonTemp = new BrightnessReason(); // Brightness animation ramp rates in brightness units per second @@ -1379,7 +1380,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // Switch to doze auto-brightness mode if needed if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null && !mAutomaticBrightnessController.isInIdleMode()) { - mAutomaticBrightnessController.switchMode(mPowerRequest.policy == POLICY_DOZE + mAutomaticBrightnessController.switchMode(Display.isDozeState(state) ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT); } @@ -1434,7 +1435,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness(); // Apply auto-brightness. int brightnessAdjustmentFlags = 0; - if (Float.isNaN(brightnessState)) { + // AutomaticBrightnessStrategy has higher priority than OffloadBrightnessStrategy + if (Float.isNaN(brightnessState) + || mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_OFFLOAD) { if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) { brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness( mTempBrightnessEvent); @@ -1455,8 +1458,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mScreenOffBrightnessSensorController != null) { mScreenOffBrightnessSensorController.setLightSensorEnabled(false); } + setBrightnessFromOffload(PowerManager.BRIGHTNESS_INVALID_FLOAT); } else { mAutomaticBrightnessStrategy.setAutoBrightnessApplied(false); + // Restore the lower-priority brightness strategy + brightnessState = displayBrightnessState.getBrightness(); } } } else { @@ -3020,9 +3026,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call setDwbcLoggingEnabled(msg.arg1); break; case MSG_SET_BRIGHTNESS_FROM_OFFLOAD: - mDisplayBrightnessController.setBrightnessFromOffload( - Float.intBitsToFloat(msg.arg1)); - updatePowerState(); + if (mDisplayBrightnessController.setBrightnessFromOffload( + Float.intBitsToFloat(msg.arg1))) { + updatePowerState(); + } break; } } diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java index f6d02dbc46df..34d53be6933a 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java @@ -26,6 +26,7 @@ import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.AutomaticBrightnessController; import com.android.server.display.BrightnessMappingStrategy; import com.android.server.display.BrightnessSetting; @@ -175,14 +176,19 @@ public final class DisplayBrightnessController { /** * Sets the brightness from the offload session. + * @return Whether the offload brightness has changed */ - public void setBrightnessFromOffload(float brightness) { + public boolean setBrightnessFromOffload(float brightness) { synchronized (mLock) { - if (mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() != null) { + if (mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() != null + && !BrightnessSynchronizer.floatEquals(mDisplayBrightnessStrategySelector + .getOffloadBrightnessStrategy().getOffloadScreenBrightness(), brightness)) { mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy() .setOffloadScreenBrightness(brightness); + return true; } } + return false; } /** diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 8e844501ab34..71cc8725f85b 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -133,7 +133,8 @@ public class DisplayBrightnessStrategySelector { } else if (BrightnessUtils.isValidBrightnessValue( mTemporaryBrightnessStrategy.getTemporaryScreenBrightness())) { displayBrightnessStrategy = mTemporaryBrightnessStrategy; - } else if (mOffloadBrightnessStrategy != null && BrightnessUtils.isValidBrightnessValue( + } else if (mAutomaticBrightnessStrategy.shouldUseAutoBrightness() + && mOffloadBrightnessStrategy != null && BrightnessUtils.isValidBrightnessValue( mOffloadBrightnessStrategy.getOffloadScreenBrightness())) { displayBrightnessStrategy = mOffloadBrightnessStrategy; } diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java index d1ca49b8bf79..8b54b22082fd 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java @@ -108,7 +108,6 @@ public class AutomaticBrightnessStrategy { mIsAutoBrightnessEnabled = shouldUseAutoBrightness() && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze) && brightnessReason != BrightnessReason.REASON_OVERRIDE - && brightnessReason != BrightnessReason.REASON_OFFLOAD && mAutomaticBrightnessController != null; mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness() && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze); diff --git a/services/core/java/com/android/server/input/debug/FocusEventDebugView.java b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java index b30f5ecb2dd5..6eae9a4bbe22 100644 --- a/services/core/java/com/android/server/input/debug/FocusEventDebugView.java +++ b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java @@ -229,7 +229,8 @@ public class FocusEventDebugView extends RelativeLayout { /** Report a key event to the debug view. */ @AnyThread public void reportKeyEvent(KeyEvent event) { - post(() -> handleKeyEvent(KeyEvent.obtain((KeyEvent) event))); + KeyEvent keyEvent = KeyEvent.obtain(event); + post(() -> handleKeyEvent(keyEvent)); } /** Report a motion event to the debug view. */ diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index fee0342a9f99..2205986fe9c9 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -39,7 +39,6 @@ import static android.server.inputmethod.InputMethodManagerServiceProto.IME_WIND import static android.server.inputmethod.InputMethodManagerServiceProto.IN_FULLSCREEN_MODE; import static android.server.inputmethod.InputMethodManagerServiceProto.IS_INTERACTIVE; import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_IME_TARGET_WINDOW_NAME; -import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_SWITCH_USER_ID; import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_IME_WITH_HARD_KEYBOARD; import static android.server.inputmethod.InputMethodManagerServiceProto.SYSTEM_READY; import static android.view.Display.DEFAULT_DISPLAY; @@ -276,9 +275,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull private final String[] mNonPreemptibleInputMethods; - @UserIdInt - private int mLastSwitchUserId; - final Context mContext; final Resources mRes; private final Handler mHandler; @@ -1311,20 +1307,35 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { + final int userId = getChangingUserId(); synchronized (ImfLock.class) { + final boolean isCurrentUser = (userId == mSettings.getUserId()); + final AdditionalSubtypeMap additionalSubtypeMap; + final InputMethodSettings settings; + if (isCurrentUser) { + additionalSubtypeMap = mAdditionalSubtypeMap; + settings = mSettings; + } else { + additionalSubtypeMap = AdditionalSubtypeUtils.load(userId); + settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); + } + // Note that one package may implement multiple IMEs. final ArrayList<String> changedImes = new ArrayList<>(); - for (InputMethodInfo imi : mSettings.getMethodList()) { + for (InputMethodInfo imi : settings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { changedImes.add(imi.getId()); } } final AdditionalSubtypeMap newMap = - mAdditionalSubtypeMap.cloneWithRemoveOrSelf(changedImes); - if (newMap != mAdditionalSubtypeMap) { - mAdditionalSubtypeMap = newMap; + additionalSubtypeMap.cloneWithRemoveOrSelf(changedImes); + if (newMap != additionalSubtypeMap) { + if (isCurrentUser) { + mAdditionalSubtypeMap = newMap; + } AdditionalSubtypeUtils.save( - mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); + newMap, settings.getMethodMap(), settings.getUserId()); } if (!changedImes.isEmpty()) { mChangedPackages.add(packageName); @@ -1649,8 +1660,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final int userId = mActivityManagerInternal.getCurrentUserId(); - mLastSwitchUserId = userId; - // mSettings should be created before buildInputMethodListLocked mSettings = InputMethodSettings.createEmptyMap(userId); @@ -1837,7 +1846,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + " selectedIme=" + mSettings.getSelectedInputMethod()); } - mLastSwitchUserId = newUserId; if (mIsInteractive && clientToBeReset != null) { final ClientState cs = mClientController.getClient(clientToBeReset.asBinder()); if (cs == null) { @@ -4710,7 +4718,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked())); proto.write(CUR_TOKEN_DISPLAY_ID, mCurTokenDisplayId); proto.write(SYSTEM_READY, mSystemReady); - proto.write(LAST_SWITCH_USER_ID, mLastSwitchUserId); proto.write(HAVE_CONNECTION, hasConnectionLocked()); proto.write(BOUND_TO_METHOD, mBoundToMethod); proto.write(IS_INTERACTIVE, mIsInteractive); @@ -6288,8 +6295,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @ShellCommandResult private int onCommandWithSystemIdentity(@Nullable String cmd) { switch (TextUtils.emptyIfNull(cmd)) { - case "get-last-switch-user-id": - return mService.getLastSwitchUserId(this); case "tracing": return mService.handleShellCommandTraceInputMethod(this); case "ime": { // For "adb shell ime <command>". @@ -6403,15 +6408,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // ---------------------------------------------------------------------- // Shell command handlers: - @BinderThread - @ShellCommandResult - private int getLastSwitchUserId(@NonNull ShellCommand shellCommand) { - synchronized (ImfLock.class) { - shellCommand.getOutPrintWriter().println(mLastSwitchUserId); - return ShellCommandResult.SUCCESS; - } - } - /** * Handles {@code adb shell ime list}. * @param shellCommand {@link ShellCommand} object that is handling this command. diff --git a/services/core/java/com/android/server/net/TEST_MAPPING b/services/core/java/com/android/server/net/TEST_MAPPING index e0376ed6461b..8db59055c1f4 100644 --- a/services/core/java/com/android/server/net/TEST_MAPPING +++ b/services/core/java/com/android/server/net/TEST_MAPPING @@ -28,5 +28,10 @@ } ] } + ], + "postsubmit":[ + { + "name":"FrameworksVpnTests" + } ] } diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index f645eaa28632..3ecc58e2aef2 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -942,6 +942,23 @@ abstract public class ManagedServices { return false; } + protected boolean isPackageOrComponentAllowedWithPermission(ComponentName component, + int userId) { + if (!(isPackageOrComponentAllowed(component.flattenToString(), userId) + || isPackageOrComponentAllowed(component.getPackageName(), userId))) { + return false; + } + return componentHasBindPermission(component, userId); + } + + private boolean componentHasBindPermission(ComponentName component, int userId) { + ServiceInfo info = getServiceInfo(component, userId); + if (info == null) { + return false; + } + return mConfig.bindPermission.equals(info.permission); + } + boolean isPackageOrComponentUserSet(String pkgOrComponent, int userId) { synchronized (mApproved) { ArraySet<String> services = mUserSetServices.get(userId); @@ -1003,6 +1020,7 @@ abstract public class ManagedServices { for (int uid : uidList) { if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) { anyServicesInvolved = true; + trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid)); } } } @@ -1135,8 +1153,7 @@ abstract public class ManagedServices { synchronized (mMutex) { if (enabled) { - if (isPackageOrComponentAllowed(component.flattenToString(), userId) - || isPackageOrComponentAllowed(component.getPackageName(), userId)) { + if (isPackageOrComponentAllowedWithPermission(component, userId)) { registerServiceLocked(component, userId); } else { Slog.d(TAG, component + " no longer has permission to be bound"); @@ -1270,6 +1287,33 @@ abstract public class ManagedServices { return removed; } + private void trimApprovedListsForInvalidServices(String packageName, int userId) { + synchronized (mApproved) { + final ArrayMap<Boolean, ArraySet<String>> approvedByType = mApproved.get(userId); + if (approvedByType == null) { + return; + } + for (int i = 0; i < approvedByType.size(); i++) { + final ArraySet<String> approved = approvedByType.valueAt(i); + for (int j = approved.size() - 1; j >= 0; j--) { + final String approvedPackageOrComponent = approved.valueAt(j); + if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) { + final ComponentName component = ComponentName.unflattenFromString( + approvedPackageOrComponent); + if (component != null && !componentHasBindPermission(component, userId)) { + approved.removeAt(j); + if (DEBUG) { + Slog.v(TAG, "Removing " + approvedPackageOrComponent + + " from approved list; no bind permission found " + + mConfig.bindPermission); + } + } + } + } + } + } + } + protected String getPackageName(String packageOrComponent) { final ComponentName component = ComponentName.unflattenFromString(packageOrComponent); if (component != null) { @@ -1519,8 +1563,7 @@ abstract public class ManagedServices { void reregisterService(final ComponentName cn, final int userId) { // If rebinding a package that died, ensure it still has permission // after the rebind delay - if (isPackageOrComponentAllowed(cn.getPackageName(), userId) - || isPackageOrComponentAllowed(cn.flattenToString(), userId)) { + if (isPackageOrComponentAllowedWithPermission(cn, userId)) { registerService(cn, userId); } } diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index b9a267f37ff9..8e37b4f24880 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -32,6 +32,7 @@ import android.app.NotificationManager; import android.content.pm.PackageManager; import android.os.Process; import android.service.notification.DNDPolicyProto; +import android.service.notification.ZenAdapters; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ConfigChangeOrigin; import android.service.notification.ZenModeConfig.ZenRule; @@ -591,9 +592,11 @@ class ZenModeEventLogger { // This applies to both call and message senders, but not conversation senders, // where they use the same enum values. proto.write(DNDPolicyProto.ALLOW_CALLS_FROM, - ZenModeConfig.getZenPolicySenders(mNewPolicy.allowCallsFrom())); + ZenAdapters.notificationPolicySendersToZenPolicyPeopleType( + mNewPolicy.allowCallsFrom())); proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM, - ZenModeConfig.getZenPolicySenders(mNewPolicy.allowMessagesFrom())); + ZenAdapters.notificationPolicySendersToZenPolicyPeopleType( + mNewPolicy.allowMessagesFrom())); proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, mNewPolicy.allowConversationsFrom()); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 6857869e3776..bc86c8216952 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -84,6 +84,7 @@ import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.ConditionProviderService; import android.service.notification.DeviceEffectsApplier; +import android.service.notification.ZenAdapters; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ConfigChangeOrigin; diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index 4f86adfe2d8d..4eb8b2b980cb 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -354,6 +354,10 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { DevicePolicyManager getDevicePolicyManager() { return mContext.getSystemService(DevicePolicyManager.class); } + + void setSystemProperty(String key, String value) { + SystemProperties.set(key, value); + } } BugreportManagerServiceImpl(Context context) { @@ -737,7 +741,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { @GuardedBy("mLock") private IDumpstate startAndGetDumpstateBinderServiceLocked() { // Start bugreport service. - SystemProperties.set("ctl.start", BUGREPORT_SERVICE); + mInjector.setSystemProperty("ctl.start", BUGREPORT_SERVICE); IDumpstate ds = null; boolean timedOut = false; @@ -769,7 +773,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { // This tells init to cancel bugreportd service. Note that this is achieved through // setting a system property which is not thread-safe. So the lock here offers // thread-safety only among callers of the API. - SystemProperties.set("ctl.stop", BUGREPORT_SERVICE); + mInjector.setSystemProperty("ctl.stop", BUGREPORT_SERVICE); } @RequiresPermission(android.Manifest.permission.DUMP) diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 9afdde53643c..b5476fdd3050 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -760,13 +760,18 @@ public class ComputerEngine implements Computer { if (pkgName == null) { if (!mCrossProfileIntentResolverEngine.shouldSkipCurrentProfile(this, intent, resolvedType, userId)) { - /* - Check for results in the current profile only if there is no - {@link CrossProfileIntentFilter} for user with flag - {@link PackageManager.SKIP_CURRENT_PROFILE} set. - */ - result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this, - intent, resolvedType, flags, userId), userId)); + + final List<ResolveInfo> queryResult = mComponentResolver.queryActivities(this, + intent, resolvedType, flags, userId); + // If the user doesn't exist, the queryResult is null + if (queryResult != null) { + /* + Check for results in the current profile only if there is no + {@link CrossProfileIntentFilter} for user with flag + {@link PackageManager.SKIP_CURRENT_PROFILE} set. + */ + result.addAll(filterIfNotSystemUser(queryResult, userId)); + } } addInstant = isInstantAppResolutionAllowed(intent, result, userId, false /*skipPackageCheck*/, flags); @@ -788,9 +793,13 @@ public class ComputerEngine implements Computer { if (setting != null && setting.getAndroidPackage() != null && (resolveForStart || !shouldFilterApplication(setting, filterCallingUid, userId))) { - result.addAll(filterIfNotSystemUser(mComponentResolver.queryActivities(this, + final List<ResolveInfo> queryResult = mComponentResolver.queryActivities(this, intent, resolvedType, flags, setting.getAndroidPackage().getActivities(), - userId), userId)); + userId); + // If the user doesn't exist, the queryResult is null + if (queryResult != null) { + result.addAll(filterIfNotSystemUser(queryResult, userId)); + } } if (result == null || result.size() == 0) { // the caller wants to resolve for a particular package; however, there diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index fe65010b7281..59d621959ac5 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3386,12 +3386,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } else if (tagName.equals("verifier")) { final String deviceIdentity = parser.getAttributeValue(null, "device"); - try { - mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity); - } catch (IllegalArgumentException e) { - Slog.w(PackageManagerService.TAG, "Discard invalid verifier device id: " - + e.getMessage()); - } + mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity); } else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) { // No longer used. } else if (tagName.equals("keyset-settings")) { @@ -3419,7 +3414,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } str.close(); - } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) { + } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException + | IllegalArgumentException e) { // Remove corrupted file and retry. atomicFile.failRead(str, e); @@ -4558,6 +4554,10 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile for (int i = 0; i < size; i++) { final PackageSetting ps = mPackages.valueAt(i); if (ps.getPkg() == null) { + // This would force-create correct per-user state. + ps.setInstalled(false, userHandle); + // Make sure the app is excluded from storage mapping for this user. + writeKernelMappingLPr(ps); continue; } final boolean shouldMaybeInstall = ps.isSystem() && diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java index f8c678aa3aa3..52ef87cf949f 100644 --- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java +++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java @@ -45,11 +45,9 @@ import static android.hardware.SensorPrivacyManager.Sources.OTHER; import static android.hardware.SensorPrivacyManager.Sources.QS_TILE; import static android.hardware.SensorPrivacyManager.Sources.SETTINGS; import static android.hardware.SensorPrivacyManager.Sources.SHELL; -import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_APPS; -import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS; -import static android.hardware.SensorPrivacyManager.StateTypes.AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS; import static android.hardware.SensorPrivacyManager.StateTypes.DISABLED; import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED; +import static android.hardware.SensorPrivacyManager.StateTypes.ENABLED_EXCEPT_ALLOWLISTED_APPS; import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_HARDWARE; import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE; import static android.os.UserHandle.USER_NULL; @@ -57,11 +55,9 @@ import static android.service.SensorPrivacyIndividualEnabledSensorProto.UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__ACTION_UNKNOWN; -import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS; -import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS; -import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON; +import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__CAMERA; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__MICROPHONE; import static com.android.internal.util.FrameworkStatsLog.PRIVACY_SENSOR_TOGGLE_INTERACTION__SENSOR__SENSOR_UNKNOWN; @@ -98,7 +94,6 @@ import android.content.pm.PackageManagerInternal; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.drawable.Icon; -import android.hardware.CameraPrivacyAllowlistEntry; import android.hardware.ISensorPrivacyListener; import android.hardware.ISensorPrivacyManager; import android.hardware.SensorPrivacyManager; @@ -153,7 +148,6 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; @@ -170,18 +164,12 @@ public final class SensorPrivacyService extends SystemService { public static final int REMINDER_DIALOG_DELAY_MILLIS = 500; @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS = - PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS; - @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS = - PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS; - @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) - private static final int ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS = - PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS; - @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) private static final int ACTION__TOGGLE_ON = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON; @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) + private static final int ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS = + PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS; + @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) private static final int ACTION__TOGGLE_OFF = PRIVACY_SENSOR_TOGGLE_INTERACTION__ACTION__TOGGLE_OFF; @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) @@ -208,8 +196,7 @@ public final class SensorPrivacyService extends SystemService { private CallStateHelper mCallStateHelper; private KeyguardManager mKeyguardManager; - List<CameraPrivacyAllowlistEntry> mCameraPrivacyAllowlist = - new ArrayList<CameraPrivacyAllowlistEntry>(); + List<String> mCameraPrivacyAllowlist = new ArrayList<String>(); private int mCurrentUser = USER_NULL; @@ -227,14 +214,8 @@ public final class SensorPrivacyService extends SystemService { mPackageManagerInternal = getLocalService(PackageManagerInternal.class); mNotificationManager = mContext.getSystemService(NotificationManager.class); mSensorPrivacyServiceImpl = new SensorPrivacyServiceImpl(); - ArrayMap<String, Boolean> cameraPrivacyAllowlist = - SystemConfig.getInstance().getCameraPrivacyAllowlist(); - - for (Map.Entry<String, Boolean> entry : cameraPrivacyAllowlist.entrySet()) { - CameraPrivacyAllowlistEntry ent = new CameraPrivacyAllowlistEntry(); - ent.packageName = entry.getKey(); - ent.isMandatory = entry.getValue(); - mCameraPrivacyAllowlist.add(ent); + for (String entry : SystemConfig.getInstance().getCameraPrivacyAllowlist()) { + mCameraPrivacyAllowlist.add(entry); } } @@ -908,14 +889,8 @@ public final class SensorPrivacyService extends SystemService { case DISABLED : logAction = ACTION__TOGGLE_ON; break; - case AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS : - logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS; - break; - case AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS : - logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS; - break; - case AUTOMOTIVE_DRIVER_ASSISTANCE_APPS : - logAction = ACTION__AUTOMOTIVE_DRIVER_ASSISTANCE_APPS; + case ENABLED_EXCEPT_ALLOWLISTED_APPS : + logAction = ACTION__TOGGLE_ON_EXCEPT_ALLOWLISTED_APPS; break; default : logAction = ACTION__ACTION_UNKNOWN; @@ -981,11 +956,23 @@ public final class SensorPrivacyService extends SystemService { @Override @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY) - public List<CameraPrivacyAllowlistEntry> getCameraPrivacyAllowlist() { + public List<String> getCameraPrivacyAllowlist() { enforceObserveSensorPrivacyPermission(); return mCameraPrivacyAllowlist; } + /** + * Sets camera privacy allowlist. + * @param allowlist List of automotive driver assistance packages for + * privacy allowlisting. + * @hide + */ + @Override + public void setCameraPrivacyAllowlist(List<String> allowlist) { + enforceManageSensorPrivacyPermission(); + mCameraPrivacyAllowlist = new ArrayList<>(allowlist); + } + @Override @FlaggedApi(Flags.FLAG_CAMERA_PRIVACY_ALLOWLIST) @RequiresPermission(Manifest.permission.OBSERVE_SENSOR_PRIVACY) @@ -1005,23 +992,9 @@ public final class SensorPrivacyService extends SystemService { return true; } else if (state == DISABLED) { return false; - } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS) { - for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) { - if ((packageName.equals(entry.packageName)) && !entry.isMandatory) { - return false; - } - } - return true; - } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS) { - for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) { - if ((packageName.equals(entry.packageName)) && entry.isMandatory) { - return false; - } - } - return true; - } else if (state == AUTOMOTIVE_DRIVER_ASSISTANCE_APPS) { - for (CameraPrivacyAllowlistEntry entry : mCameraPrivacyAllowlist) { - if (packageName.equals(entry.packageName)) { + } else if (state == ENABLED_EXCEPT_ALLOWLISTED_APPS) { + for (String entry : mCameraPrivacyAllowlist) { + if (packageName.equals(entry)) { return false; } } @@ -1616,20 +1589,7 @@ public final class SensorPrivacyService extends SystemService { setToggleSensorPrivacy(userId, SHELL, sensor, false); } break; - case "automotive_driver_assistance_apps" : { - if (Flags.cameraPrivacyAllowlist()) { - int sensor = sensorStrToId(getNextArgRequired()); - if ((!isAutomotive(mContext)) || (sensor != CAMERA)) { - pw.println("Command not valid for this sensor"); - return -1; - } - - setToggleSensorPrivacyState(userId, SHELL, sensor, - AUTOMOTIVE_DRIVER_ASSISTANCE_APPS); - } - } - break; - case "automotive_driver_assistance_helpful_apps" : { + case "enable_except_allowlisted_apps" : { if (Flags.cameraPrivacyAllowlist()) { int sensor = sensorStrToId(getNextArgRequired()); if ((!isAutomotive(mContext)) || (sensor != CAMERA)) { @@ -1638,20 +1598,7 @@ public final class SensorPrivacyService extends SystemService { } setToggleSensorPrivacyState(userId, SHELL, sensor, - AUTOMOTIVE_DRIVER_ASSISTANCE_HELPFUL_APPS); - } - } - break; - case "automotive_driver_assistance_required_apps" : { - if (Flags.cameraPrivacyAllowlist()) { - int sensor = sensorStrToId(getNextArgRequired()); - if ((!isAutomotive(mContext)) || (sensor != CAMERA)) { - pw.println("Command not valid for this sensor"); - return -1; - } - - setToggleSensorPrivacyState(userId, SHELL, sensor, - AUTOMOTIVE_DRIVER_ASSISTANCE_REQUIRED_APPS); + ENABLED_EXCEPT_ALLOWLISTED_APPS); } } break; @@ -1679,18 +1626,9 @@ public final class SensorPrivacyService extends SystemService { pw.println(""); if (Flags.cameraPrivacyAllowlist()) { if (isAutomotive(mContext)) { - pw.println(" automotive_driver_assistance_apps USER_ID SENSOR"); - pw.println(" Disable privacy for automotive apps which help you" - + " drive and apps which are required by OEM"); - pw.println(""); - pw.println(" automotive_driver_assistance_helpful_apps " - + "USER_ID SENSOR"); - pw.println(" Disable privacy for automotive apps which " - + "help you drive."); - pw.println(""); - pw.println(" automotive_driver_assistance_required_apps " + pw.println(" enable_except_allowlisted_apps " + "USER_ID SENSOR"); - pw.println(" Disable privacy for automotive apps which are " + pw.println(" Enable privacy except for automotive apps which are " + "required by OEM."); pw.println(""); } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 601c7f450d4f..5175b74116f4 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -39,6 +39,7 @@ import android.util.SparseArray; import android.view.DisplayInfo; import android.view.View; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.utils.TimingsTraceAndSlog; import libcore.io.IoUtils; @@ -65,7 +66,7 @@ public class WallpaperCropper { * Maximum acceptable parallax. * A value of 1 means "the additional width for parallax is at most 100% of the screen width" */ - private static final float MAX_PARALLAX = 1f; + @VisibleForTesting static final float MAX_PARALLAX = 1f; /** * We define three ways to adjust a crop. These modes are used depending on the situation: @@ -73,10 +74,9 @@ public class WallpaperCropper { * - When going from folded to unfolded, we want to add content * - For a screen rotation, we want to keep the same amount of content */ - private static final int ADD = 1; - private static final int REMOVE = 2; - private static final int BALANCE = 3; - + @VisibleForTesting static final int ADD = 1; + @VisibleForTesting static final int REMOVE = 2; + @VisibleForTesting static final int BALANCE = 3; private final WallpaperDisplayHelper mWallpaperDisplayHelper; @@ -131,19 +131,23 @@ public class WallpaperCropper { (bitmapSize.y - crop.height()) / 2); return crop; } + + // If any suggested crop is invalid, fallback to case 1 + for (int i = 0; i < suggestedCrops.size(); i++) { + Rect testCrop = suggestedCrops.valueAt(i); + if (testCrop == null || testCrop.left < 0 || testCrop.top < 0 + || testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) { + Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize); + return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + } + } + int orientation = getOrientation(displaySize); // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop Rect suggestedCrop = suggestedCrops.get(orientation); if (suggestedCrop != null) { - if (suggestedCrop.left < 0 || suggestedCrop.top < 0 - || suggestedCrop.right > bitmapSize.x || suggestedCrop.bottom > bitmapSize.y) { - Slog.w(TAG, "invalid suggested crop: " + suggestedCrop); - Rect fullImage = new Rect(0, 0, bitmapSize.x, bitmapSize.y); - return getAdjustedCrop(fullImage, bitmapSize, displaySize, true, rtl, ADD); - } else { return getAdjustedCrop(suggestedCrop, bitmapSize, displaySize, true, rtl, ADD); - } } // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and @@ -209,7 +213,8 @@ public class WallpaperCropper { * Given a crop, a displaySize for the orientation of that crop, compute the visible part of the * crop. This removes any additional width used for parallax. No-op if displaySize == null. */ - private static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) { + @VisibleForTesting + static Rect noParallax(Rect crop, Point displaySize, Point bitmapSize, boolean rtl) { if (displaySize == null) return crop; Rect adjustedCrop = getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); // only keep the visible part (without parallax) @@ -240,12 +245,14 @@ public class WallpaperCropper { * </li> * </ul> */ - private static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, + @VisibleForTesting + static Rect getAdjustedCrop(Rect crop, Point bitmapSize, Point screenSize, boolean parallax, boolean rtl, int mode) { Rect adjustedCrop = new Rect(crop); float cropRatio = ((float) crop.width()) / crop.height(); float screenRatio = ((float) screenSize.x) / screenSize.y; - if (cropRatio >= screenRatio) { + if (cropRatio == screenRatio) return crop; + if (cropRatio > screenRatio) { if (!parallax) { // rotate everything 90 degrees clockwise, compute the result, and rotate back int newLeft = bitmapSize.y - crop.bottom; @@ -274,6 +281,7 @@ public class WallpaperCropper { } } } else { + // TODO (b/281648899) the third case is not always correct, fix that. int widthToAdd = mode == REMOVE ? 0 : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width()) : (int) (0.5 + crop.height() - crop.width()); @@ -644,6 +652,9 @@ public class WallpaperCropper { if (!success) { Slog.e(TAG, "Unable to apply new wallpaper"); wallpaper.getCropFile().delete(); + wallpaper.mCropHints.clear(); + wallpaper.cropHint.set(0, 0, 0, 0); + wallpaper.mSampleSize = 1f; } if (wallpaper.getCropFile().exists()) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index 88e9672cd0a1..0165d65283dc 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -341,6 +341,7 @@ public class WallpaperDataParser { } else { wallpaper.cropHint.set(totalCropHint); } + wallpaper.mSampleSize = parser.getAttributeFloat(null, "sampleSize", 1f); } else { wallpaper.cropHint.set(totalCropHint); } @@ -493,6 +494,7 @@ public class WallpaperDataParser { out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top); out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right); out.attributeInt(null, "totalCropBottom", wallpaper.cropHint.bottom); + out.attributeFloat(null, "sampleSize", wallpaper.mSampleSize); } else if (!multiCrop()) { final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(DEFAULT_DISPLAY); diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index e9c40964aee4..043470f62850 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -30,6 +30,7 @@ import android.os.PatternMatcher; import android.os.Process; import android.os.ResultReceiver; import android.os.ShellCallback; +import android.os.SystemClock; import android.os.UserHandle; import android.util.Slog; import android.webkit.IWebViewUpdateService; @@ -37,6 +38,7 @@ import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; import com.android.internal.util.DumpUtils; +import com.android.modules.expresslog.Histogram; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -52,6 +54,14 @@ public class WebViewUpdateService extends SystemService { private static final String TAG = "WebViewUpdateService"; + private static final Histogram sPrepareWebViewInSystemServerLatency = new Histogram( + "webview.value_prepare_webview_in_system_server_latency", + new Histogram.ScaledRangeOptions(20, 0, 1, 1.5f)); + + private static final Histogram sAppWaitingForRelroCompletionDelay = new Histogram( + "webview.value_app_waiting_for_relro_completion_delay", + new Histogram.ScaledRangeOptions(20, 0, 1, 1.4f)); + private BroadcastReceiver mWebViewUpdatedReceiver; private WebViewUpdateServiceInterface mImpl; @@ -132,7 +142,10 @@ public class WebViewUpdateService extends SystemService { } public void prepareWebViewInSystemServer() { + long currentTimeMs = SystemClock.uptimeMillis(); mImpl.prepareWebViewInSystemServer(); + sPrepareWebViewInSystemServerLatency.logSample( + (float) (SystemClock.uptimeMillis() - currentTimeMs)); } private static String packageNameFromIntent(Intent intent) { @@ -204,8 +217,12 @@ public class WebViewUpdateService extends SystemService { throw new IllegalStateException("Cannot create a WebView from the SystemServer"); } + long startTimeMs = SystemClock.uptimeMillis(); final WebViewProviderResponse webViewProviderResponse = WebViewUpdateService.this.mImpl.waitForAndGetProvider(); + long endTimeMs = SystemClock.uptimeMillis(); + sAppWaitingForRelroCompletionDelay.logSample((float) (endTimeMs - startTimeMs)); + if (webViewProviderResponse.packageInfo != null) { grantVisibilityToCaller( webViewProviderResponse.packageInfo.packageName, Binder.getCallingUid()); diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index 1d6ad6d3a6d9..532ff984ae56 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -23,12 +23,15 @@ import android.content.pm.Signature; import android.os.AsyncTask; import android.os.Trace; import android.os.UserHandle; +import android.util.AndroidRuntimeException; import android.util.Slog; import android.webkit.UserPackage; import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; +import com.android.modules.expresslog.Counter; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -357,6 +360,12 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { mNumRelroCreationsFinished = 0; mNumRelroCreationsStarted = mSystemInterface.onWebViewProviderChanged(newPackage); + Counter.logIncrement("webview.value_on_webview_provider_changed_counter"); + if (newPackage.packageName.equals(getDefaultWebViewPackage().packageName)) { + Counter.logIncrement( + "webview.value_on_webview_provider_changed_" + + "with_default_package_counter"); + } // If the relro creations finish before we know the number of started creations // we will have to do any cleanup/notifying here. checkIfRelrosDoneLocked(); @@ -388,9 +397,15 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public WebViewProviderInfo getDefaultWebViewPackage() { - throw new IllegalStateException( - "getDefaultWebViewPackage shouldn't be called if update_service_v2 flag is" - + " disabled."); + for (WebViewProviderInfo provider : getWebViewPackages()) { + if (provider.availableByDefault) { + return provider; + } + } + + // This should be unreachable because the config parser enforces that there is at least + // one availableByDefault provider. + throw new AndroidRuntimeException("No available by default WebView Provider."); } private static class ProviderAndPackageInfo { diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index 1f9d265a0113..fb338ba245e1 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -31,6 +31,8 @@ import android.webkit.WebViewFactory; import android.webkit.WebViewProviderInfo; import android.webkit.WebViewProviderResponse; +import com.android.modules.expresslog.Counter; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -412,6 +414,12 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mNumRelroCreationsFinished = 0; mNumRelroCreationsStarted = mSystemInterface.onWebViewProviderChanged(newPackage); + Counter.logIncrement("webview.value_on_webview_provider_changed_counter"); + if (newPackage.packageName.equals(getDefaultWebViewPackage().packageName)) { + Counter.logIncrement( + "webview.value_on_webview_provider_changed_" + + "with_default_package_counter"); + } // If the relro creations finish before we know the number of started creations // we will have to do any cleanup/notifying here. checkIfRelrosDoneLocked(); @@ -479,6 +487,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { * for all users, otherwise use the default provider. */ private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { + Counter.logIncrement("webview.value_find_preferred_webview_package_counter"); // If the user has chosen provider, use that (if it's installed and enabled for all // users). String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext); @@ -508,12 +517,15 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(mDefaultProvider); if (validityResult(mDefaultProvider, packageInfo) == VALIDITY_OK) { return packageInfo; + } else { + Counter.logIncrement("webview.value_default_webview_package_invalid_counter"); } } catch (NameNotFoundException e) { Slog.w(TAG, "Default WebView package (" + mDefaultProvider.packageName + ") not found"); } // This should never happen during normal operation (only with modified system images). + Counter.logIncrement("webview.value_webview_not_usable_for_all_users_counter"); mAnyWebViewInstalled = false; throw new WebViewPackageMissingException("Could not find a loadable WebView package"); } diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 91eff188107f..418998870f16 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -1209,6 +1209,7 @@ final class AccessibilityController { private boolean mShown; private boolean mLastSurfaceShown; private int mAlpha; + private int mPreviousAlpha; private volatile boolean mInvalidated; @@ -1344,6 +1345,7 @@ final class AccessibilityController { // using WindowManagerGlobalLock. Grab copies of these values before // drawing on the canvas so that drawing can be performed outside of the lock. int alpha; + boolean redrawBounds; Rect drawingRect = null; Region drawingBounds = null; synchronized (mService.mGlobalLock) { @@ -1361,7 +1363,13 @@ final class AccessibilityController { mInvalidated = false; alpha = mAlpha; - if (alpha > 0) { + // For b/325863281, we should ensure the drawn border path is cleared when + // alpha = 0. Therefore, we cache the last used alpha when drawing as + // mPreviousAlpha and check it here. If mPreviousAlpha > 0, which means + // the border is showing now, then we should still redraw the clear path + // on the canvas so the border is cleared. + redrawBounds = mAlpha > 0 || mPreviousAlpha > 0; + if (redrawBounds) { drawingBounds = new Region(mBounds); // Empty dirty rectangle means unspecified. if (mDirtyRect.isEmpty()) { @@ -1378,7 +1386,7 @@ final class AccessibilityController { final boolean showSurface; // Draw without holding WindowManagerGlobalLock. - if (alpha > 0) { + if (redrawBounds) { Canvas canvas = null; try { canvas = mSurface.lockCanvas(drawingRect); @@ -1392,11 +1400,11 @@ final class AccessibilityController { mPaint.setAlpha(alpha); canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint); mSurface.unlockCanvasAndPost(canvas); - showSurface = true; - } else { - showSurface = false; + mPreviousAlpha = alpha; } + showSurface = alpha > 0; + if (showSurface && !mLastSurfaceShown) { mTransaction.show(mSurfaceControl).apply(); mLastSurfaceShown = true; diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index fa76774a604f..83f44d23dbb1 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -311,7 +311,7 @@ class InsetsSourceProvider { return mInsetsHint; } final WindowState win = mWindowContainer.asWindowState(); - if (win != null && win.mGivenInsetsPending && win.mAttrs.providedInsets == null) { + if (win != null && win.mGivenInsetsPending) { return mInsetsHint; } if (mInsetsHintStale) { @@ -520,37 +520,11 @@ class InsetsSourceProvider { updateVisibility(); mControl = new InsetsSourceControl(mSource.getId(), mSource.getType(), leash, mClientVisible, surfacePosition, getInsetsHint()); - mStateController.notifySurfaceTransactionReady(this, getSurfaceTransactionId(leash), true); ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource Control %s for target %s", mControl, mControlTarget); } - private long getSurfaceTransactionId(SurfaceControl leash) { - // Here returns mNativeObject (long) as the ID instead of the leash itself so that - // InsetsStateController won't keep referencing the leash unexpectedly. - return leash != null ? leash.mNativeObject : 0; - } - - /** - * This is called when the surface transaction of the leash initialization has been committed. - * - * @param id Indicates which transaction is committed so that stale callbacks can be dropped. - */ - void onSurfaceTransactionCommitted(long id) { - if (mIsLeashReadyForDispatching) { - return; - } - if (mControl == null) { - return; - } - if (id != getSurfaceTransactionId(mControl.getLeash())) { - return; - } - mIsLeashReadyForDispatching = true; - mStateController.notifySurfaceTransactionReady(this, 0, false); - } - void startSeamlessRotation() { if (!mSeamlessRotating) { mSeamlessRotating = true; @@ -571,6 +545,10 @@ class InsetsSourceProvider { return true; } + void onSurfaceTransactionApplied() { + mIsLeashReadyForDispatching = true; + } + void setClientVisible(boolean clientVisible) { if (mClientVisible == clientVisible) { return; @@ -755,7 +733,6 @@ class InsetsSourceProvider { public void onAnimationCancelled(SurfaceControl animationLeash) { if (mAdapter == this) { mStateController.notifyControlRevoked(mControlTarget, InsetsSourceProvider.this); - mStateController.notifySurfaceTransactionReady(InsetsSourceProvider.this, 0, false); mControl = null; mControlTarget = null; mAdapter = null; diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index ba578f642429..6b9fcf411ce1 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -34,7 +34,6 @@ import android.os.Trace; import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseArray; -import android.util.SparseLongArray; import android.util.proto.ProtoOutputStream; import android.view.InsetsSource; import android.view.InsetsSourceControl; @@ -59,7 +58,6 @@ class InsetsStateController { private final DisplayContent mDisplayContent; private final SparseArray<InsetsSourceProvider> mProviders = new SparseArray<>(); - private final SparseLongArray mSurfaceTransactionIds = new SparseLongArray(); private final ArrayMap<InsetsControlTarget, ArrayList<InsetsSourceProvider>> mControlTargetProvidersMap = new ArrayMap<>(); private final SparseArray<InsetsControlTarget> mIdControlTargetMap = new SparseArray<>(); @@ -362,32 +360,14 @@ class InsetsStateController { notifyPendingInsetsControlChanged(); } - void notifySurfaceTransactionReady(InsetsSourceProvider provider, long id, boolean ready) { - if (ready) { - mSurfaceTransactionIds.put(provider.getSource().getId(), id); - } else { - mSurfaceTransactionIds.delete(provider.getSource().getId()); - } - } - private void notifyPendingInsetsControlChanged() { if (mPendingControlChanged.isEmpty()) { return; } - final int size = mSurfaceTransactionIds.size(); - final SparseLongArray surfaceTransactionIds = new SparseLongArray(size); - for (int i = 0; i < size; i++) { - surfaceTransactionIds.append( - mSurfaceTransactionIds.keyAt(i), mSurfaceTransactionIds.valueAt(i)); - } mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> { - for (int i = 0; i < size; i++) { - final int sourceId = surfaceTransactionIds.keyAt(i); - final InsetsSourceProvider provider = mProviders.get(sourceId); - if (provider == null) { - continue; - } - provider.onSurfaceTransactionCommitted(surfaceTransactionIds.valueAt(i)); + for (int i = mProviders.size() - 1; i >= 0; i--) { + final InsetsSourceProvider provider = mProviders.valueAt(i); + provider.onSurfaceTransactionApplied(); } final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>(); int displayId = mDisplayContent.getDisplayId(); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 1353ff09b292..e16d869fdcd9 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3751,9 +3751,11 @@ class Task extends TaskFragment { // Boost the adjacent TaskFragment for dimmer if needed. final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && taskFragment.isEmbedded()) { + taskFragment.mDimmerSurfaceBoosted = false; final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) { adjacentTf.assignLayer(t, layer++); + adjacentTf.mDimmerSurfaceBoosted = true; } } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 78ababc6473f..a818a721f964 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -216,6 +216,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { Dimmer mDimmer = Dimmer.DIMMER_REFACTOR ? new SmoothDimmer(this) : new LegacyDimmer(this); + /** {@code true} if the dimmer surface is boosted. {@code false} otherwise. */ + boolean mDimmerSurfaceBoosted; + /** Apply the dim layer on the embedded TaskFragment. */ static final int EMBEDDED_DIM_AREA_TASK_FRAGMENT = 0; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 594043d380c9..9b19a707d7be 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -349,7 +349,7 @@ class WallpaperController { final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); int screenWidth = lastWallpaperBounds.width(); int screenHeight = lastWallpaperBounds.height(); - float screenRatio = ((float) screenWidth) / screenHeight; + float screenRatio = (float) screenWidth / screenHeight; Point screenSize = new Point(screenWidth, screenHeight); WallpaperWindowToken token = wallpaperWin.mToken.asWallpaperToken(); @@ -399,20 +399,32 @@ class WallpaperController { Point bitmapSize = new Point( wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight); SparseArray<Rect> cropHints = token.getCropHints(); - wallpaperFrame = mWallpaperCropUtils.getCrop( - screenSize, bitmapSize, cropHints, wallpaperWin.isRtl()); - - cropZoom = wallpaperFrame.isEmpty() ? 1f - : ((float) screenHeight) / wallpaperFrame.height() / wallpaperWin.mVScale; - - // A positive x / y offset shifts the wallpaper to the right / bottom respectively. - cropOffsetX = -wallpaperFrame.left - + (int) ((cropZoom - 1f) * wallpaperFrame.height() * screenRatio / 2f); - cropOffsetY = -wallpaperFrame.top - + (int) ((cropZoom - 1f) * wallpaperFrame.height() / 2f); - - diffWidth = (int) (wallpaperFrame.width() * wallpaperWin.mHScale) - screenWidth; - diffHeight = (int) (wallpaperFrame.height() * wallpaperWin.mVScale) - screenHeight; + wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame() + : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints, + wallpaperWin.isRtl()); + int frameWidth = wallpaperFrame.width(); + int frameHeight = wallpaperFrame.height(); + float frameRatio = (float) frameWidth / frameHeight; + + // If the crop is proportionally wider/taller than the screen, scale it so that its + // height/width matches the screen height/width, and use the additional width/height + // for parallax (respectively). + boolean scaleHeight = frameRatio >= screenRatio; + cropZoom = wallpaperFrame.isEmpty() ? 1f : scaleHeight + ? (float) screenHeight / frameHeight / wallpaperWin.mVScale + : (float) screenWidth / frameWidth / wallpaperWin.mHScale; + + // The dimensions of the frame, without the additional width or height for parallax. + float w = scaleHeight ? frameHeight * screenRatio : frameWidth; + float h = scaleHeight ? frameHeight : frameWidth / screenRatio; + + // Note: a positive x/y offset shifts the wallpaper to the right/bottom respectively. + cropOffsetX = -wallpaperFrame.left + (int) ((cropZoom - 1f) * w / 2f); + cropOffsetY = -wallpaperFrame.top + (int) ((cropZoom - 1f) * h / 2f); + + // Available width or height for parallax + diffWidth = (int) ((frameWidth - w) * wallpaperWin.mHScale); + diffHeight = (int) ((frameHeight - h) * wallpaperWin.mVScale); } else { wallpaperFrame = wallpaperWin.getFrame(); cropZoom = 1f; diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 5e7f1cbdd06e..b43a4540bbde 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -28,7 +28,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.content.Context; -import android.os.HandlerExecutor; import android.os.Trace; import android.util.Slog; import android.util.TimeUtils; @@ -70,8 +69,6 @@ public class WindowAnimator { private Choreographer mChoreographer; - private final HandlerExecutor mExecutor; - /** * Indicates whether we have an animation frame callback scheduled, which will happen at * vsync-app and then schedule the animation tick at the right time (vsync-sf). @@ -83,7 +80,8 @@ public class WindowAnimator { * A list of runnable that need to be run after {@link WindowContainer#prepareSurfaces} is * executed and the corresponding transaction is closed and applied. */ - private ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>(); + private final ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>(); + private boolean mInExecuteAfterPrepareSurfacesRunnables; private final SurfaceControl.Transaction mTransaction; @@ -94,7 +92,6 @@ public class WindowAnimator { mTransaction = service.mTransactionFactory.get(); service.mAnimationHandler.runWithScissors( () -> mChoreographer = Choreographer.getSfInstance(), 0 /* timeout */); - mExecutor = new HandlerExecutor(service.mAnimationHandler); mAnimationFrameCallback = frameTimeNs -> { synchronized (mService.mGlobalLock) { @@ -200,19 +197,6 @@ public class WindowAnimator { updateRunningExpensiveAnimationsLegacy(); } - final ArrayList<Runnable> afterPrepareSurfacesRunnables = mAfterPrepareSurfacesRunnables; - if (!afterPrepareSurfacesRunnables.isEmpty()) { - mAfterPrepareSurfacesRunnables = new ArrayList<>(); - mTransaction.addTransactionCommittedListener(mExecutor, () -> { - synchronized (mService.mGlobalLock) { - // Traverse in order they were added. - for (int i = 0, size = afterPrepareSurfacesRunnables.size(); i < size; i++) { - afterPrepareSurfacesRunnables.get(i).run(); - } - afterPrepareSurfacesRunnables.clear(); - } - }); - } Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyTransaction"); mTransaction.apply(); Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); @@ -220,6 +204,7 @@ public class WindowAnimator { ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate"); mService.mAtmService.mTaskOrganizerController.dispatchPendingEvents(); + executeAfterPrepareSurfacesRunnables(); if (DEBUG_WINDOW_TRACE) { Slog.i(TAG, "!!! animate: exit" @@ -301,10 +286,34 @@ public class WindowAnimator { /** * Adds a runnable to be executed after {@link WindowContainer#prepareSurfaces} is called and - * the corresponding transaction is closed, applied, and committed. + * the corresponding transaction is closed and applied. */ void addAfterPrepareSurfacesRunnable(Runnable r) { + // If runnables are already being handled in executeAfterPrepareSurfacesRunnable, then just + // immediately execute the runnable passed in. + if (mInExecuteAfterPrepareSurfacesRunnables) { + r.run(); + return; + } + mAfterPrepareSurfacesRunnables.add(r); scheduleAnimation(); } + + void executeAfterPrepareSurfacesRunnables() { + + // Don't even think about to start recursing! + if (mInExecuteAfterPrepareSurfacesRunnables) { + return; + } + mInExecuteAfterPrepareSurfacesRunnables = true; + + // Traverse in order they were added. + final int size = mAfterPrepareSurfacesRunnables.size(); + for (int i = 0; i < size; i++) { + mAfterPrepareSurfacesRunnables.get(i).run(); + } + mAfterPrepareSurfacesRunnables.clear(); + mInExecuteAfterPrepareSurfacesRunnables = false; + } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index ae5a5cb7316c..a055db274ae8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2109,7 +2109,15 @@ public class WindowManagerService extends IWindowManager.Stub + ", touchableRegion=" + w.mGivenTouchableRegion + " -> " + touchableRegion + ", touchableInsets " + w.mTouchableInsets + " -> " + touchableInsets); if (w != null) { + final boolean wasGivenInsetsPending = w.mGivenInsetsPending; w.mGivenInsetsPending = false; + if ((!wasGivenInsetsPending || !w.hasInsetsSourceProvider()) + && w.mTouchableInsets == touchableInsets + && w.mGivenContentInsets.equals(contentInsets) + && w.mGivenVisibleInsets.equals(visibleInsets) + && w.mGivenTouchableRegion.equals(touchableRegion)) { + return; + } w.mGivenContentInsets.set(contentInsets); w.mGivenVisibleInsets.set(visibleInsets); w.mGivenTouchableRegion.set(touchableRegion); @@ -9214,6 +9222,11 @@ public class WindowManagerService extends IWindowManager.Stub return false; } + if (taskFragment.mDimmerSurfaceBoosted) { + // Skip if the TaskFragment currently has dimmer surface boosted. + return false; + } + final ActivityRecord topActivity = taskFragment.getTask().topRunningActivity(true /* focusableOnly */); if (topActivity == null || topActivity == focusedWindow.mActivityRecord) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 18ac0e748536..1106a95fcdce 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1342,7 +1342,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // This window doesn't provide any insets. return; } - if (mGivenInsetsPending && mAttrs.providedInsets == null) { + if (mGivenInsetsPending) { // The given insets are pending, and they are not reliable for now. The source frame // should be updated after the new given insets are sent to window manager. return; diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index 6d5fc80b8d1e..b0e71bda787a 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -109,6 +109,9 @@ class WindowTracing { return; } synchronized (mEnabledLock) { + if (!android.tracing.Flags.perfettoProtologTracing()) { + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw); + } logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); mBuffer.resetBuffer(); mEnabled = mEnabledLockFree = true; @@ -136,6 +139,9 @@ class WindowTracing { writeTraceToFileLocked(); logAndPrintln(pw, "Trace written to " + mTraceFile + "."); } + if (!android.tracing.Flags.perfettoProtologTracing()) { + ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true); + } } /** diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp index ccd9bd0a50ca..b2bdaa35f28c 100644 --- a/services/core/jni/com_android_server_hint_HintManagerService.cpp +++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp @@ -24,16 +24,17 @@ #include <nativehelper/JNIHelp.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <powermanager/PowerHalController.h> +#include <powermanager/PowerHintSessionWrapper.h> #include <utils/Log.h> #include <unordered_map> #include "jni.h" -using aidl::android::hardware::power::IPowerHintSession; using aidl::android::hardware::power::SessionHint; using aidl::android::hardware::power::SessionMode; using aidl::android::hardware::power::WorkDuration; +using android::power::PowerHintSessionWrapper; using android::base::StringPrintf; @@ -49,7 +50,7 @@ static struct { } gWorkDurationInfo; static power::PowerHalController gPowerHalController; -static std::unordered_map<jlong, std::shared_ptr<IPowerHintSession>> gSessionMap; +static std::unordered_map<jlong, std::shared_ptr<PowerHintSessionWrapper>> gSessionMap; static std::mutex gSessionMapLock; static int64_t getHintSessionPreferredRate() { @@ -76,45 +77,45 @@ static jlong createHintSession(JNIEnv* env, int32_t tgid, int32_t uid, } static void pauseHintSession(JNIEnv* env, int64_t session_ptr) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->pause(); } static void resumeHintSession(JNIEnv* env, int64_t session_ptr) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->resume(); } static void closeHintSession(JNIEnv* env, int64_t session_ptr) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->close(); std::unique_lock<std::mutex> sessionLock(gSessionMapLock); gSessionMap.erase(session_ptr); } static void updateTargetWorkDuration(int64_t session_ptr, int64_t targetDurationNanos) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->updateTargetWorkDuration(targetDurationNanos); } static void reportActualWorkDuration(int64_t session_ptr, const std::vector<WorkDuration>& actualDurations) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->reportActualWorkDuration(actualDurations); } static void sendHint(int64_t session_ptr, SessionHint hint) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->sendHint(hint); } static void setThreads(int64_t session_ptr, const std::vector<int32_t>& threadIds) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->setThreads(threadIds); } static void setMode(int64_t session_ptr, SessionMode mode, bool enabled) { - auto appSession = reinterpret_cast<IPowerHintSession*>(session_ptr); + auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr); appSession->setMode(mode, enabled); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 80046b6075ee..c37946b4d750 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -23389,7 +23389,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG); } - private boolean isUnicornFlagEnabled() { + static boolean isUnicornFlagEnabled() { return false; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java index c108deaf33bc..a7adc5b6d925 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java @@ -66,6 +66,10 @@ final class PolicyEnforcerCallbacks { private static final String LOG_TAG = "PolicyEnforcerCallbacks"; static boolean setAutoTimezoneEnabled(@Nullable Boolean enabled, @NonNull Context context) { + if (!DevicePolicyManagerService.isUnicornFlagEnabled()) { + Slogf.w(LOG_TAG, "Trying to enforce setAutoTimezoneEnabled while flag is off."); + return true; + } return Binder.withCleanCallingIdentity(() -> { Objects.requireNonNull(context); @@ -79,6 +83,10 @@ final class PolicyEnforcerCallbacks { static boolean setPermissionGrantState( @Nullable Integer grantState, @NonNull Context context, int userId, @NonNull PolicyKey policyKey) { + if (!DevicePolicyManagerService.isUnicornFlagEnabled()) { + Slogf.w(LOG_TAG, "Trying to enforce setPermissionGrantState while flag is off."); + return true; + } return Boolean.TRUE.equals(Binder.withCleanCallingIdentity(() -> { if (!(policyKey instanceof PackagePermissionPolicyKey)) { throw new IllegalArgumentException("policyKey is not of type " diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java index 82d5247ebed8..209107e50902 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java @@ -37,6 +37,7 @@ import android.os.Handler; import android.util.ArraySet; import android.util.Dumpable; import android.view.Display; +import android.view.DisplayInfo; import android.view.Surface; import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; @@ -65,6 +66,7 @@ public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceSt private final Handler mHandler = new Handler(); private final PostureEstimator mPostureEstimator; private final DisplayManager mDisplayManager; + private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo(); /** * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair @@ -140,10 +142,11 @@ public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceSt public void onDisplayChanged(int displayId) { if (displayId == DEFAULT_DISPLAY) { final Display display = mDisplayManager.getDisplay(displayId); + display.getDisplayInfo(mDefaultDisplayInfo); int displayState = display.getState(); boolean isDisplayOn = displayState == Display.STATE_ON; mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn); - mPostureEstimator.onDisplayRotationChanged(display.getRotation()); + mPostureEstimator.onDisplayRotationChanged(mDefaultDisplayInfo.rotation); } } diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java index 8d01b7a9c523..901f24dd9b0b 100644 --- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java +++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java @@ -48,6 +48,7 @@ import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.view.Display; +import android.view.DisplayInfo; import android.view.Surface; import androidx.test.platform.app.InstrumentationRegistry; @@ -629,7 +630,11 @@ public final class BookStyleDeviceStatePolicyTest { } private void sendScreenRotation(int rotation) { - when(mDisplay.getRotation()).thenReturn(rotation); + doAnswer(invocation -> { + final DisplayInfo displayInfo = invocation.getArgument(0); + displayInfo.rotation = rotation; + return null; + }).when(mDisplay).getDisplayInfo(any()); mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY)); } diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt index fc2eb2652d2d..c0d988d0c46b 100644 --- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt +++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt @@ -20,27 +20,63 @@ import android.app.AppOpsManager import android.companion.virtual.VirtualDeviceManager import android.os.Handler import android.os.UserHandle +import android.permission.flags.Flags import android.util.ArrayMap import android.util.ArraySet +import android.util.LongSparseArray +import android.util.Slog +import android.util.SparseArray import android.util.SparseBooleanArray import android.util.SparseIntArray import com.android.internal.annotations.VisibleForTesting +import com.android.internal.util.IntPair import com.android.server.appop.AppOpsCheckingServiceInterface import com.android.server.appop.AppOpsCheckingServiceInterface.AppOpsModeChangedListener import com.android.server.permission.access.AccessCheckingService import com.android.server.permission.access.AppOpUri +import com.android.server.permission.access.DevicePermissionUri +import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.PackageUri +import com.android.server.permission.access.PermissionUri import com.android.server.permission.access.UidUri +import com.android.server.permission.access.appop.AppOpModes.MODE_ALLOWED +import com.android.server.permission.access.appop.AppOpModes.MODE_FOREGROUND +import com.android.server.permission.access.appop.AppOpModes.MODE_IGNORED import com.android.server.permission.access.collection.forEachIndexed import com.android.server.permission.access.collection.set +import com.android.server.permission.access.permission.AppIdPermissionPolicy +import com.android.server.permission.access.permission.DevicePermissionPolicy +import com.android.server.permission.access.permission.PermissionFlags +import com.android.server.permission.access.permission.PermissionService class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingServiceInterface { private val packagePolicy = service.getSchemePolicy(PackageUri.SCHEME, AppOpUri.SCHEME) as PackageAppOpPolicy private val appIdPolicy = service.getSchemePolicy(UidUri.SCHEME, AppOpUri.SCHEME) as AppIdAppOpPolicy + private val permissionPolicy = + service.getSchemePolicy(UidUri.SCHEME, PermissionUri.SCHEME) as AppIdPermissionPolicy + private val devicePermissionPolicy = + service.getSchemePolicy(UidUri.SCHEME, DevicePermissionUri.SCHEME) as DevicePermissionPolicy private val context = service.context + + // Maps appop code to its runtime permission + private val runtimeAppOpToPermissionNames = SparseArray<String>() + + // Maps runtime permission to its appop codes + private val runtimePermissionNameToAppOp = ArrayMap<String, Int>() + + private var foregroundableOps = SparseBooleanArray() + + /* Maps foreground permissions to their background permission. Background permissions aren't + required to be runtime */ + private val foregroundToBackgroundPermissionName = ArrayMap<String, String>() + + /* Maps background permissions to their foreground permissions. Background permissions aren't + required to be runtime */ + private val backgroundToForegroundPermissionNames = ArrayMap<String, ArraySet<String>>() + private lateinit var handler: Handler @Volatile private var listeners = ArraySet<AppOpsModeChangedListener>() @@ -69,11 +105,60 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } override fun systemReady() { - // Not implemented because upgrades are handled automatically. + if (Flags.runtimePermissionAppopsMappingEnabled()) { + createPermissionAppOpMapping() + val permissionListener = OnPermissionFlagsChangedListener() + permissionPolicy.addOnPermissionFlagsChangedListener(permissionListener) + devicePermissionPolicy.addOnPermissionFlagsChangedListener(permissionListener) + } + } + + private fun createPermissionAppOpMapping() { + val permissions = service.getState { with(permissionPolicy) { getPermissions() } } + + for (appOpCode in 0 until AppOpsManager._NUM_OP) { + AppOpsManager.opToPermission(appOpCode)?.let { permissionName -> + // Multiple ops might map to a single permission but only one is considered the + // runtime appop calculations. + if (appOpCode == AppOpsManager.permissionToOpCode(permissionName)) { + val permission = permissions[permissionName]!! + if (permission.isRuntime) { + runtimePermissionNameToAppOp[permissionName] = appOpCode + runtimeAppOpToPermissionNames[appOpCode] = permissionName + permission.permissionInfo.backgroundPermission?.let { + backgroundPermissionName -> + // Note: background permission may not be runtime, + // e.g. microphone/camera. + foregroundableOps[appOpCode] = true + foregroundToBackgroundPermissionName[permissionName] = + backgroundPermissionName + backgroundToForegroundPermissionNames + .getOrPut(backgroundPermissionName, ::ArraySet) + .add(permissionName) + } + } + } + } + } } override fun getNonDefaultUidModes(uid: Int, persistentDeviceId: String): SparseIntArray { - return opNameMapToOpSparseArray(getUidModes(uid)) + val appId = UserHandle.getAppId(uid) + val userId = UserHandle.getUserId(uid) + service.getState { + val modes = + with(appIdPolicy) { opNameMapToOpSparseArray(getAppOpModes(appId, userId)?.map) } + if (Flags.runtimePermissionAppopsMappingEnabled()) { + runtimePermissionNameToAppOp.forEachIndexed { _, permissionName, appOpCode -> + val mode = getUidModeFromPermissionState(appId, userId, permissionName) + if (mode != AppOpsManager.opToDefaultMode(appOpCode)) { + modes[appOpCode] = mode + } + } + } + + return modes + } } override fun getNonDefaultPackageModes(packageName: String, userId: Int): SparseIntArray { @@ -84,7 +169,13 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS val appId = UserHandle.getAppId(uid) val userId = UserHandle.getUserId(uid) val opName = AppOpsManager.opToPublicName(op) - return service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } } + val permissionName = runtimeAppOpToPermissionNames[op] + + return if (!Flags.runtimePermissionAppopsMappingEnabled() || permissionName == null) { + service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } } + } else { + service.getState { getUidModeFromPermissionState(appId, userId, permissionName) } + } } private fun getUidModes(uid: Int): ArrayMap<String, Int>? { @@ -93,13 +184,66 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS return service.getState { with(appIdPolicy) { getAppOpModes(appId, userId) } }?.map } - override fun setUidMode(uid: Int, persistentDeviceId: String, op: Int, mode: Int): Boolean { + private fun GetStateScope.getUidModeFromPermissionState( + appId: Int, + userId: Int, + permissionName: String + ): Int { + val permissionFlags = + with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) } + val backgroundPermissionName = foregroundToBackgroundPermissionName[permissionName] + val backgroundPermissionFlags = + if (backgroundPermissionName != null) { + with(permissionPolicy) { + getPermissionFlags(appId, userId, backgroundPermissionName) + } + } else { + PermissionFlags.RUNTIME_GRANTED + } + val result = evaluateModeFromPermissionFlags(permissionFlags, backgroundPermissionFlags) + if (result != MODE_IGNORED) { + return result + } + + val fullerPermissionName = + PermissionService.getFullerPermission(permissionName) ?: return result + return getUidModeFromPermissionState(appId, userId, fullerPermissionName) + } + + private fun evaluateModeFromPermissionFlags( + foregroundFlags: Int, + backgroundFlags: Int = PermissionFlags.RUNTIME_GRANTED + ): Int = + if (PermissionFlags.isAppOpGranted(foregroundFlags)) { + if (PermissionFlags.isAppOpGranted(backgroundFlags)) { + MODE_ALLOWED + } else { + MODE_FOREGROUND + } + } else { + MODE_IGNORED + } + + override fun setUidMode(uid: Int, persistentDeviceId: String, code: Int, mode: Int): Boolean { + if ( + Flags.runtimePermissionAppopsMappingEnabled() && code in runtimeAppOpToPermissionNames + ) { + Slog.w( + LOG_TAG, + "Cannot set UID mode for runtime permission app op, uid = $uid," + + " code = ${AppOpsManager.opToName(code)}," + + " mode = ${AppOpsManager.modeToName(mode)}", + RuntimeException() + ) + return false + } + val appId = UserHandle.getAppId(uid) val userId = UserHandle.getUserId(uid) - val opName = AppOpsManager.opToPublicName(op) - var wasChanged = false + val appOpName = AppOpsManager.opToPublicName(code) + var wasChanged: Boolean service.mutateState { - wasChanged = with(appIdPolicy) { setAppOpMode(appId, userId, opName, mode) } + wasChanged = with(appIdPolicy) { setAppOpMode(appId, userId, appOpName, mode) } } return wasChanged } @@ -114,10 +258,23 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS private fun getPackageModes(packageName: String, userId: Int): ArrayMap<String, Int>? = service.getState { with(packagePolicy) { getAppOpModes(packageName, userId) } }?.map - override fun setPackageMode(packageName: String, op: Int, mode: Int, userId: Int) { - val opName = AppOpsManager.opToPublicName(op) + override fun setPackageMode(packageName: String, appOpCode: Int, mode: Int, userId: Int) { + val appOpName = AppOpsManager.opToPublicName(appOpCode) + + if ( + Flags.runtimePermissionAppopsMappingEnabled() && + appOpCode in runtimeAppOpToPermissionNames + ) { + Slog.w( + LOG_TAG, + "(packageName=$packageName, userId=$userId)'s appop state" + + " for runtime op $appOpName should not be set directly.", + RuntimeException() + ) + return + } service.mutateState { - with(packagePolicy) { setAppOpMode(packageName, userId, opName, mode) } + with(packagePolicy) { setAppOpMode(packageName, userId, appOpName, mode) } } } @@ -128,7 +285,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } override fun removePackage(packageName: String, userId: Int): Boolean { - var wasChanged = false + var wasChanged: Boolean service.mutateState { wasChanged = with(packagePolicy) { removeAppOpModes(packageName, userId) } } @@ -158,6 +315,13 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS this[AppOpsManager.strOpToOp(op)] = true } } + if (Flags.runtimePermissionAppopsMappingEnabled()) { + foregroundableOps.forEachIndexed { _, op, _ -> + if (getUidMode(uid, persistentDeviceId, op) == AppOpsManager.MODE_FOREGROUND) { + this[op] = true + } + } + } } } @@ -168,6 +332,13 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS this[AppOpsManager.strOpToOp(op)] = true } } + if (Flags.runtimePermissionAppopsMappingEnabled()) { + foregroundableOps.forEachIndexed { _, op, _ -> + if (getPackageMode(packageName, op, userId) == AppOpsManager.MODE_FOREGROUND) { + this[op] = true + } + } + } } } @@ -189,9 +360,10 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } } - inner class OnAppIdAppOpModeChangedListener : AppIdAppOpPolicy.OnAppOpModeChangedListener() { + private inner class OnAppIdAppOpModeChangedListener : + AppIdAppOpPolicy.OnAppOpModeChangedListener() { // (uid, appOpCode) -> newMode - val pendingChanges = ArrayMap<Pair<Int, Int>, Int>() + private val pendingChanges = LongSparseArray<Int>() override fun onAppOpModeChanged( appId: Int, @@ -202,7 +374,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS ) { val uid = UserHandle.getUid(userId, appId) val appOpCode = AppOpsManager.strOpToOp(appOpName) - val key = Pair(uid, appOpCode) + val key = IntPair.of(uid, appOpCode) pendingChanges[key] = newMode } @@ -211,13 +383,15 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS val listenersLocal = listeners pendingChanges.forEachIndexed { _, key, mode -> listenersLocal.forEachIndexed { _, listener -> - val uid = key.first - val appOpCode = key.second + val uid = IntPair.first(key) + val appOpCode = IntPair.second(key) - listener.onUidModeChanged(uid, + listener.onUidModeChanged( + uid, appOpCode, mode, - VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT) + VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT + ) } } @@ -228,7 +402,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS private inner class OnPackageAppOpModeChangedListener : PackageAppOpPolicy.OnAppOpModeChangedListener() { // (packageName, userId, appOpCode) -> newMode - val pendingChanges = ArrayMap<Triple<String, Int, Int>, Int>() + private val pendingChanges = ArrayMap<Triple<String, Int, Int>, Int>() override fun onAppOpModeChanged( packageName: String, @@ -258,4 +432,130 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS pendingChanges.clear() } } + + private inner class OnPermissionFlagsChangedListener : + AppIdPermissionPolicy.OnPermissionFlagsChangedListener, + DevicePermissionPolicy.OnDevicePermissionFlagsChangedListener { + // (uid, deviceId, appOpCode) -> newMode + private val pendingChanges = ArrayMap<Triple<Int, String, Int>, Int>() + + override fun onPermissionFlagsChanged( + appId: Int, + userId: Int, + permissionName: String, + oldFlags: Int, + newFlags: Int + ) { + onDevicePermissionFlagsChanged( + appId, + userId, + VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT, + permissionName, + oldFlags, + newFlags + ) + } + + override fun onDevicePermissionFlagsChanged( + appId: Int, + userId: Int, + deviceId: String, + permissionName: String, + oldFlags: Int, + newFlags: Int + ) { + backgroundToForegroundPermissionNames[permissionName]?.let { foregroundPermissions -> + // This is a background permission; there may be multiple foreground permissions + // affected. + foregroundPermissions.forEachIndexed { _, foregroundPermissionName -> + runtimePermissionNameToAppOp[foregroundPermissionName]?.let { appOpCode -> + val foregroundPermissionFlags = + getPermissionFlags(appId, userId, foregroundPermissionName) + addPendingChangedModeIfNeeded( + appId, + userId, + deviceId, + appOpCode, + foregroundPermissionFlags, + oldFlags, + foregroundPermissionFlags, + newFlags + ) + } + } + } + ?: foregroundToBackgroundPermissionName[permissionName]?.let { backgroundPermission + -> + runtimePermissionNameToAppOp[permissionName]?.let { appOpCode -> + val backgroundPermissionFlags = + getPermissionFlags(appId, userId, backgroundPermission) + addPendingChangedModeIfNeeded( + appId, + userId, + deviceId, + appOpCode, + oldFlags, + backgroundPermissionFlags, + newFlags, + backgroundPermissionFlags + ) + } + } + ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode -> + addPendingChangedModeIfNeeded( + appId, + userId, + deviceId, + appOpCode, + oldFlags, + PermissionFlags.RUNTIME_GRANTED, + newFlags, + PermissionFlags.RUNTIME_GRANTED + ) + } + } + + private fun getPermissionFlags(appId: Int, userId: Int, permissionName: String): Int = + service.getState { + with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) } + } + + private fun addPendingChangedModeIfNeeded( + appId: Int, + userId: Int, + deviceId: String, + appOpCode: Int, + oldForegroundFlags: Int, + oldBackgroundFlags: Int, + newForegroundFlags: Int, + newBackgroundFlags: Int, + ) { + val oldMode = evaluateModeFromPermissionFlags(oldForegroundFlags, oldBackgroundFlags) + val newMode = evaluateModeFromPermissionFlags(newForegroundFlags, newBackgroundFlags) + + if (oldMode != newMode) { + val uid = UserHandle.getUid(userId, appId) + pendingChanges[Triple(uid, deviceId, appOpCode)] = newMode + } + } + + override fun onStateMutated() { + val listenersLocal = listeners + pendingChanges.forEachIndexed { _, key, mode -> + listenersLocal.forEachIndexed { _, listener -> + val uid = key.first + val deviceId = key.second + val appOpCode = key.third + + listener.onUidModeChanged(uid, appOpCode, mode, deviceId) + } + } + + pendingChanges.clear() + } + } + + companion object { + private val LOG_TAG = AppOpService::class.java.simpleName + } } diff --git a/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt b/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt new file mode 100644 index 000000000000..827dd0e5d292 --- /dev/null +++ b/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 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.permission.access.collection + +import android.util.LongSparseArray + +inline fun <T> LongSparseArray<T>.allIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (!predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun <T> LongSparseArray<T>.anyIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return true + } + } + return false +} + +inline fun <T> LongSparseArray<T>.forEachIndexed(action: (Int, Long, T) -> Unit) { + for (index in 0 until size) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun <T> LongSparseArray<T>.forEachReversedIndexed(action: (Int, Long, T) -> Unit) { + for (index in lastIndex downTo 0) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun <T> LongSparseArray<T>.getOrPut(key: Long, defaultValue: () -> T): T { + val index = indexOfKey(key) + return if (index >= 0) { + valueAt(index) + } else { + defaultValue().also { put(key, it) } + } +} + +inline val <T> LongSparseArray<T>.lastIndex: Int + get() = size - 1 + +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> LongSparseArray<T>.minusAssign(key: Long) { + delete(key) +} + +inline fun <T> LongSparseArray<T>.noneIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun <T> LongSparseArray<T>.removeAllIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline fun <T> LongSparseArray<T>.retainAllIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (!predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline val <T> LongSparseArray<T>.size: Int + get() = size() + +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) { + put(key, value) +} diff --git a/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt b/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt new file mode 100644 index 000000000000..a582431aa83c --- /dev/null +++ b/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt @@ -0,0 +1,120 @@ +/* + * 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.permission.access.collection + +import android.util.SparseIntArray + +inline fun SparseIntArray.allIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (!predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun SparseIntArray.anyIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return true + } + } + return false +} + +inline fun SparseIntArray.forEachIndexed(action: (Int, Int, Int) -> Unit) { + for (index in 0 until size) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun SparseIntArray.forEachReversedIndexed(action: (Int, Int, Int) -> Unit) { + for (index in lastIndex downTo 0) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun SparseIntArray.getOrPut(key: Int, defaultValue: () -> Int): Int { + val index = indexOfKey(key) + return if (index >= 0) { + valueAt(index) + } else { + defaultValue().also { put(key, it) } + } +} + +inline val SparseIntArray.lastIndex: Int + get() = size - 1 + +@Suppress("NOTHING_TO_INLINE") +inline operator fun SparseIntArray.minusAssign(key: Int) { + delete(key) +} + +inline fun SparseIntArray.noneIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return false + } + } + return true +} + +fun SparseIntArray.remove(key: Int) { + delete(key) +} + +fun SparseIntArray.remove(key: Int, defaultValue: Int): Int { + val index = indexOfKey(key) + return if (index >= 0) { + val oldValue = valueAt(index) + removeAt(index) + oldValue + } else { + defaultValue + } +} + +inline fun SparseIntArray.removeAllIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline fun SparseIntArray.retainAllIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (!predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun SparseIntArray.set(key: Int, value: Int) { + put(key, value) +} + +inline val SparseIntArray.size: Int + get() = size() diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index 1f654631f902..0b58543e076d 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -2826,5 +2826,8 @@ class PermissionService(private val service: AccessCheckingService) : } else { emptySet<String>() } + + fun getFullerPermission(permissionName: String): String? = + FULLER_PERMISSIONS[permissionName] } } diff --git a/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java new file mode 100644 index 000000000000..180f54e1cf5f --- /dev/null +++ b/services/tests/VpnTests/java/android/net/Ikev2VpnProfileTest.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS; +import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6; +import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.ipsec.ike.IkeKeyIdIdentification; +import android.net.ipsec.ike.IkeTunnelConnectionParams; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.net.VpnProfile; +import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator; +import com.android.net.module.util.ProxyUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.security.auth.x500.X500Principal; + +/** Unit tests for {@link Ikev2VpnProfile.Builder}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class Ikev2VpnProfileTest { + private static final String SERVER_ADDR_STRING = "1.2.3.4"; + private static final String IDENTITY_STRING = "Identity"; + private static final String USERNAME_STRING = "username"; + private static final String PASSWORD_STRING = "pa55w0rd"; + private static final String EXCL_LIST = "exclList"; + private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); + private static final int TEST_MTU = 1300; + + private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy( + SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST)); + + private X509Certificate mUserCert; + private X509Certificate mServerRootCa; + private PrivateKey mPrivateKey; + + @Before + public void setUp() throws Exception { + mServerRootCa = generateRandomCertAndKeyPair().cert; + + final CertificateAndKey userCertKey = generateRandomCertAndKeyPair(); + mUserCert = userCertKey.cert; + mPrivateKey = userCertKey.key; + } + + private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING); + + builder.setBypassable(true); + builder.setProxy(mProxy); + builder.setMaxMtu(TEST_MTU); + builder.setMetered(true); + + return builder; + } + + @Test + public void testBuildValidProfileWithOptions() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + // Check non-auth parameters correctly stored + assertEquals(SERVER_ADDR_STRING, profile.getServerAddr()); + assertEquals(IDENTITY_STRING, profile.getUserIdentity()); + assertEquals(mProxy, profile.getProxyInfo()); + assertTrue(profile.isBypassable()); + assertTrue(profile.isMetered()); + assertEquals(TEST_MTU, profile.getMaxMtu()); + assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms()); + } + + @Test + public void testBuildUsernamePasswordProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertEquals(USERNAME_STRING, profile.getUsername()); + assertEquals(PASSWORD_STRING, profile.getPassword()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + assertNull(profile.getPresharedKey()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildDigitalSignatureProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertEquals(profile.getUserCert(), mUserCert); + assertEquals(mPrivateKey, profile.getRsaPrivateKey()); + assertEquals(profile.getServerRootCaCert(), mServerRootCa); + + assertNull(profile.getPresharedKey()); + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + } + + @Test + public void testBuildPresharedKeyProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertArrayEquals(PSK_BYTES, profile.getPresharedKey()); + + assertNull(profile.getServerRootCaCert()); + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildWithAllowedAlgorithmsAead() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + + List<String> allowedAlgorithms = + Arrays.asList( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305); + builder.setAllowedAlgorithms(allowedAlgorithms); + + final Ikev2VpnProfile profile = builder.build(); + assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms()); + } + + @Test + public void testBuildWithAllowedAlgorithmsNormal() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + + List<String> allowedAlgorithms = + Arrays.asList( + IpSecAlgorithm.AUTH_HMAC_SHA512, + IpSecAlgorithm.AUTH_AES_XCBC, + IpSecAlgorithm.AUTH_AES_CMAC, + IpSecAlgorithm.CRYPT_AES_CBC, + IpSecAlgorithm.CRYPT_AES_CTR); + builder.setAllowedAlgorithms(allowedAlgorithms); + + final Ikev2VpnProfile profile = builder.build(); + assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms()); + } + + @Test + public void testSetAllowedAlgorithmsEmptyList() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setAllowedAlgorithms(new ArrayList<>()); + fail("Expected exception due to no valid algorithm set"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetAllowedAlgorithmsInvalidList() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256)); + fail("Expected exception due to missing encryption"); + } catch (IllegalArgumentException expected) { + } + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC)); + fail("Expected exception due to missing authentication"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5)); + fail("Expected exception due to insecure algorithm"); + } catch (IllegalArgumentException expected) { + } + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1)); + fail("Expected exception due to insecure algorithm"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildNoAuthMethodSet() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.build(); + fail("Expected exception due to lack of auth method"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildExcludeLocalRoutesSet() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + builder.setLocalRoutesExcluded(true); + + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + assertTrue(profile.areLocalRoutesExcluded()); + + builder.setBypassable(false); + try { + builder.build(); + fail("Expected exception because excludeLocalRoutes should be set only" + + " on the bypassable VPN"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildInvalidMtu() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setMaxMtu(500); + fail("Expected exception due to too-small MTU"); + } catch (IllegalArgumentException expected) { + } + } + + private void verifyVpnProfileCommon(VpnProfile profile) { + assertEquals(SERVER_ADDR_STRING, profile.server); + assertEquals(IDENTITY_STRING, profile.ipsecIdentifier); + assertEquals(mProxy, profile.proxy); + assertTrue(profile.isBypassable); + assertTrue(profile.isMetered); + assertEquals(TEST_MTU, profile.maxMtu); + } + + @Test + public void testPskConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final VpnProfile profile = builder.build().toVpnProfile(); + + verifyVpnProfileCommon(profile); + assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret); + + // Check nothing else is set + assertEquals("", profile.username); + assertEquals("", profile.password); + assertEquals("", profile.ipsecUserCert); + assertEquals("", profile.ipsecCaCert); + } + + @Test + public void testUsernamePasswordConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + + verifyVpnProfileCommon(profile); + assertEquals(USERNAME_STRING, profile.username); + assertEquals(PASSWORD_STRING, profile.password); + assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); + + // Check nothing else is set + assertEquals("", profile.ipsecUserCert); + assertEquals("", profile.ipsecSecret); + } + + @Test + public void testRsaConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + + final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE + + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded()); + verifyVpnProfileCommon(profile); + assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert); + assertEquals( + expectedSecret, + profile.ipsecSecret); + assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); + + // Check nothing else is set + assertEquals("", profile.username); + assertEquals("", profile.password); + } + + @Test + public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.username = USERNAME_STRING; + profile.password = PASSWORD_STRING; + profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa); + profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert); + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getUsername()); + assertNull(result.getPassword()); + assertNull(result.getUserCert()); + assertNull(result.getRsaPrivateKey()); + assertNull(result.getServerRootCaCert()); + } + + @Test + public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.ipsecSecret = new String(PSK_BYTES); + profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert); + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getPresharedKey()); + assertNull(result.getUserCert()); + assertNull(result.getRsaPrivateKey()); + } + + @Test + public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.username = USERNAME_STRING; + profile.password = PASSWORD_STRING; + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getUsername()); + assertNull(result.getPassword()); + assertNull(result.getPresharedKey()); + } + + @Test + public void testPskConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testUsernamePasswordConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testRsaConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testBuildWithIkeTunConnParamsConvertToVpnProfile() throws Exception { + // Special keyId that contains delimiter character of VpnProfile + final byte[] keyId = "foo\0bar".getBytes(); + final IkeTunnelConnectionParams tunnelParams = new IkeTunnelConnectionParams( + getTestIkeSessionParams(true /* testIpv6 */, new IkeKeyIdIdentification(keyId)), + CHILD_PARAMS); + final Ikev2VpnProfile ikev2VpnProfile = new Ikev2VpnProfile.Builder(tunnelParams).build(); + final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile(); + + assertEquals(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS, vpnProfile.type); + + // Username, password, server, ipsecIdentifier, ipsecCaCert, ipsecSecret, ipsecUserCert and + // getAllowedAlgorithms should not be set if IkeTunnelConnectionParams is set. + assertEquals("", vpnProfile.server); + assertEquals("", vpnProfile.ipsecIdentifier); + assertEquals("", vpnProfile.username); + assertEquals("", vpnProfile.password); + assertEquals("", vpnProfile.ipsecCaCert); + assertEquals("", vpnProfile.ipsecSecret); + assertEquals("", vpnProfile.ipsecUserCert); + assertEquals(0, vpnProfile.getAllowedAlgorithms().size()); + + // IkeTunnelConnectionParams should stay the same. + assertEquals(tunnelParams, vpnProfile.ikeTunConnParams); + + // Convert to disk-stable format and then back to Ikev2VpnProfile should be the same. + final VpnProfile decodedVpnProfile = + VpnProfile.decode(vpnProfile.key, vpnProfile.encode()); + final Ikev2VpnProfile convertedIkev2VpnProfile = + Ikev2VpnProfile.fromVpnProfile(decodedVpnProfile); + assertEquals(ikev2VpnProfile, convertedIkev2VpnProfile); + } + + @Test + public void testConversionIsLosslessWithIkeTunConnParams() throws Exception { + final IkeTunnelConnectionParams tunnelParams = + new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS); + // Config authentication related fields is not required while building with + // IkeTunnelConnectionParams. + final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build(); + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAutomaticNattKeepaliveTimerEnabled(true); + builder.setAutomaticIpVersionSelectionEnabled(true); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testAutomaticNattAndIpVersionDefaults() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled()); + assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled()); + } + + @Test + public void testEquals() throws Exception { + // Verify building without IkeTunnelConnectionParams + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + assertEquals(builder.build(), builder.build()); + + // Verify building with IkeTunnelConnectionParams + final IkeTunnelConnectionParams tunnelParams = + new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS); + final IkeTunnelConnectionParams tunnelParams2 = + new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS); + assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(), + new Ikev2VpnProfile.Builder(tunnelParams2).build()); + } + + @Test + public void testBuildProfileWithNullProxy() throws Exception { + final Ikev2VpnProfile ikev2VpnProfile = + new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING) + .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa) + .build(); + + // ProxyInfo should be null for the profile without setting ProxyInfo. + assertNull(ikev2VpnProfile.getProxyInfo()); + + // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile() + final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile(); + assertNull(vpnProfile.proxy); + + final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile); + assertNull(convertedIkev2VpnProfile.getProxyInfo()); + } + + private static class CertificateAndKey { + public final X509Certificate cert; + public final PrivateKey key; + + CertificateAndKey(X509Certificate cert, PrivateKey key) { + this.cert = cert; + this.key = key; + } + } + + private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception { + final Date validityBeginDate = + new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L)); + final Date validityEndDate = + new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L)); + + // Generate a keypair + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(512); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + final X500Principal dnName = new X500Principal("CN=test.android.com"); + final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setSubjectDN(dnName); + certGen.setIssuerDN(dnName); + certGen.setNotBefore(validityBeginDate); + certGen.setNotAfter(validityEndDate); + certGen.setPublicKey(keyPair.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + + final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL"); + return new CertificateAndKey(cert, keyPair.getPrivate()); + } +} diff --git a/services/tests/VpnTests/java/android/net/VpnManagerTest.java b/services/tests/VpnTests/java/android/net/VpnManagerTest.java new file mode 100644 index 000000000000..f5b83f0ee79d --- /dev/null +++ b/services/tests/VpnTests/java/android/net/VpnManagerTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.test.mock.MockContext; +import android.util.SparseArray; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.net.VpnProfile; +import com.android.internal.util.MessageUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link VpnManager}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VpnManagerTest { + + private static final String PKG_NAME = "fooPackage"; + + private static final String SERVER_ADDR_STRING = "1.2.3.4"; + private static final String IDENTITY_STRING = "Identity"; + private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); + + private IVpnManager mMockService; + private VpnManager mVpnManager; + private final MockContext mMockContext = + new MockContext() { + @Override + public String getOpPackageName() { + return PKG_NAME; + } + }; + + @Before + public void setUp() throws Exception { + assumeFalse("Skipping test because watches don't support VPN", + InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_WATCH)); + mMockService = mock(IVpnManager.class); + mVpnManager = new VpnManager(mMockContext, mMockService); + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final PlatformVpnProfile profile = getPlatformVpnProfile(); + when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))) + .thenReturn(true); + + // Expect there to be no intent returned, as consent has already been granted. + assertNull(mVpnManager.provisionVpnProfile(profile)); + verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); + } + + @Test + public void testProvisionVpnProfileNeedsConsent() throws Exception { + final PlatformVpnProfile profile = getPlatformVpnProfile(); + when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))) + .thenReturn(false); + + // Expect intent to be returned, as consent has not already been granted. + final Intent intent = mVpnManager.provisionVpnProfile(profile); + assertNotNull(intent); + + final ComponentName expectedComponentName = + ComponentName.unflattenFromString( + "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog"); + assertEquals(expectedComponentName, intent.getComponent()); + verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); + } + + @Test + public void testDeleteProvisionedVpnProfile() throws Exception { + mVpnManager.deleteProvisionedVpnProfile(); + verify(mMockService).deleteVpnProfile(eq(PKG_NAME)); + } + + @Test + public void testStartProvisionedVpnProfile() throws Exception { + mVpnManager.startProvisionedVpnProfile(); + verify(mMockService).startVpnProfile(eq(PKG_NAME)); + } + + @Test + public void testStopProvisionedVpnProfile() throws Exception { + mVpnManager.stopProvisionedVpnProfile(); + verify(mMockService).stopVpnProfile(eq(PKG_NAME)); + } + + private Ikev2VpnProfile getPlatformVpnProfile() throws Exception { + return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING) + .setBypassable(true) + .setMaxMtu(1300) + .setMetered(true) + .setAuthPsk(PSK_BYTES) + .build(); + } + + @Test + public void testVpnTypesEqual() throws Exception { + SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames( + new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" }); + SparseArray<String> nativeVpnType = MessageUtils.findMessageNames( + new Class[] { NativeVpnType.class }, new String[]{ "" }); + + // TYPE_VPN_NONE = -1 is only defined in VpnManager. + assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size()); + for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) { + assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i)); + } + } +} diff --git a/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java new file mode 100644 index 000000000000..acbe8b858d8f --- /dev/null +++ b/services/tests/VpnTests/java/com/android/internal/net/VpnProfileTest.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.net; + +import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS; +import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.net.IpSecAlgorithm; +import android.net.ipsec.ike.IkeTunnelConnectionParams; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Unit tests for {@link VpnProfile}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VpnProfileTest { + private static final String DUMMY_PROFILE_KEY = "Test"; + + private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23; + private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24; + private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25; + private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26; + private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27; + private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28; + private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29; + + @Test + public void testDefaults() throws Exception { + final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY); + + assertEquals(DUMMY_PROFILE_KEY, p.key); + assertEquals("", p.name); + assertEquals(VpnProfile.TYPE_PPTP, p.type); + assertEquals("", p.server); + assertEquals("", p.username); + assertEquals("", p.password); + assertEquals("", p.dnsServers); + assertEquals("", p.searchDomains); + assertEquals("", p.routes); + assertTrue(p.mppe); + assertEquals("", p.l2tpSecret); + assertEquals("", p.ipsecIdentifier); + assertEquals("", p.ipsecSecret); + assertEquals("", p.ipsecUserCert); + assertEquals("", p.ipsecCaCert); + assertEquals("", p.ipsecServerCert); + assertEquals(null, p.proxy); + assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty()); + assertFalse(p.isBypassable); + assertFalse(p.isMetered); + assertEquals(1360, p.maxMtu); + assertFalse(p.areAuthParamsInline); + assertFalse(p.isRestrictedToTestNetworks); + assertFalse(p.excludeLocalRoutes); + assertFalse(p.requiresInternetValidation); + assertFalse(p.automaticNattKeepaliveTimerEnabled); + assertFalse(p.automaticIpVersionSelectionEnabled); + } + + private VpnProfile getSampleIkev2Profile(String key) { + final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */, + false /* excludesLocalRoutes */, true /* requiresPlatformValidation */, + null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */, + true /* automaticIpVersionSelectionEnabled */); + + p.name = "foo"; + p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + p.server = "bar"; + p.username = "baz"; + p.password = "qux"; + p.dnsServers = "8.8.8.8"; + p.searchDomains = ""; + p.routes = "0.0.0.0/0"; + p.mppe = false; + p.l2tpSecret = ""; + p.ipsecIdentifier = "quux"; + p.ipsecSecret = "quuz"; + p.ipsecUserCert = "corge"; + p.ipsecCaCert = "grault"; + p.ipsecServerCert = "garply"; + p.proxy = null; + p.setAllowedAlgorithms( + Arrays.asList( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, + IpSecAlgorithm.AUTH_HMAC_SHA512, + IpSecAlgorithm.CRYPT_AES_CBC)); + p.isBypassable = true; + p.isMetered = true; + p.maxMtu = 1350; + p.areAuthParamsInline = true; + + // Not saved, but also not compared. + p.saveLogin = true; + + return p; + } + + private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) { + final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */, + false /* excludesLocalRoutes */, true /* requiresPlatformValidation */, + new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS), + true /* mAutomaticNattKeepaliveTimerEnabled */, + true /* automaticIpVersionSelectionEnabled */); + + p.name = "foo"; + p.server = "bar"; + p.dnsServers = "8.8.8.8"; + p.searchDomains = ""; + p.routes = "0.0.0.0/0"; + p.mppe = false; + p.proxy = null; + p.setAllowedAlgorithms( + Arrays.asList( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, + IpSecAlgorithm.AUTH_HMAC_SHA512, + IpSecAlgorithm.CRYPT_AES_CBC)); + p.isBypassable = true; + p.isMetered = true; + p.maxMtu = 1350; + p.areAuthParamsInline = true; + + // Not saved, but also not compared. + p.saveLogin = true; + + return p; + } + + @Test + public void testEquals() { + assertEquals( + getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY)); + + final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + modified.maxMtu--; + assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified); + } + + @Test + public void testParcelUnparcel() { + assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28); + assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28); + } + + @Test + public void testEncodeDecodeWithIkeTunConnParams() { + final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY); + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertEquals(profile, decoded); + } + + @Test + public void testEncodeDecode() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertEquals(profile, decoded); + } + + @Test + public void testEncodeDecodeTooManyValues() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final byte[] tooManyValues = + (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes(); + + assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues)); + } + + private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) { + // Sort to ensure when we remove, we can do it from greatest first. + Arrays.sort(missingIndices); + + final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode()); + final List<String> parts = + new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER))); + + // Remove from back first to ensure indexing is consistent. + for (int i = missingIndices.length - 1; i >= 0; i--) { + parts.remove(missingIndices[i]); + } + + return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0])); + } + + @Test + public void testEncodeDecodeInvalidNumberOfValues() { + final String tooFewValues = + getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_AUTH_PARAMS_INLINE, + ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS, + ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE, + ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION, + ENCODED_INDEX_IKE_TUN_CONN_PARAMS, + ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED, + ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED + /* missingIndices */); + + assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes())); + } + + private String getEncodedDecodedIkev2ProfileWithtooFewValues() { + return getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS, + ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE, + ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION, + ENCODED_INDEX_IKE_TUN_CONN_PARAMS, + ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED, + ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */); + } + + @Test + public void testEncodeDecodeMissingIsRestrictedToTestNetworks() { + final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues(); + + // Verify decoding without isRestrictedToTestNetworks defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.isRestrictedToTestNetworks); + } + + @Test + public void testEncodeDecodeMissingExcludeLocalRoutes() { + final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues(); + + // Verify decoding without excludeLocalRoutes defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.excludeLocalRoutes); + } + + @Test + public void testEncodeDecodeMissingRequiresValidation() { + final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues(); + + // Verify decoding without requiresValidation defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.requiresInternetValidation); + } + + @Test + public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() { + final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues(); + + // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.automaticNattKeepaliveTimerEnabled); + } + + @Test + public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() { + final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues(); + + // Verify decoding without automaticIpVersionSelectionEnabled defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.automaticIpVersionSelectionEnabled); + } + + @Test + public void testEncodeDecodeLoginsNotSaved() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + profile.saveLogin = false; + + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertNotEquals(profile, decoded); + + // Add the username/password back, everything else must be equal. + decoded.username = profile.username; + decoded.password = profile.password; + assertEquals(profile, decoded); + } + + @Test + public void testClone() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final VpnProfile clone = profile.clone(); + assertEquals(profile, clone); + assertNotSame(profile, clone); + } +} diff --git a/services/tests/servicestests/src/com/android/server/net/LockdownVpnTrackerTest.java b/services/tests/VpnTests/java/com/android/server/net/LockdownVpnTrackerTest.java index 0e881efd4cdf..0e881efd4cdf 100644 --- a/services/tests/servicestests/src/com/android/server/net/LockdownVpnTrackerTest.java +++ b/services/tests/VpnTests/java/com/android/server/net/LockdownVpnTrackerTest.java diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java index fbb14c3db9f9..440905137cf9 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.hardware.display.DisplayManagerInternal; -import android.os.PowerManager; import org.junit.Before; import org.junit.Test; @@ -66,8 +65,6 @@ public class DisplayOffloadSessionImplTest { mSession.stopOffload(); assertFalse(mSession.isActive()); - verify(mDisplayPowerController).setBrightnessFromOffload( - PowerManager.BRIGHTNESS_INVALID_FLOAT); // An inactive session shouldn't be stopped again mSession.stopOffload(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index e9315c8ed8e6..14d8a9c5f0f2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -22,6 +22,7 @@ import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIG import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -77,6 +78,7 @@ import com.android.server.LocalServices; import com.android.server.am.BatteryStatsService; import com.android.server.display.RampAnimator.DualRampAnimator; import com.android.server.display.brightness.BrightnessEvent; +import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.clamper.BrightnessClamperController; import com.android.server.display.brightness.clamper.HdrClamper; import com.android.server.display.color.ColorDisplayService; @@ -1559,6 +1561,43 @@ public final class DisplayPowerControllerTest { verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false)); + assertEquals(BrightnessReason.REASON_OFFLOAD, mHolder.dpc.mBrightnessReason.getReason()); + } + + @Test + public void testBrightness_AutomaticHigherPriorityThanOffload() { + when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); + mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); + Settings.System.putInt(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); + float brightness = 0.34f; + when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f); + when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON); + when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness( + any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT); + mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); + + mHolder.dpc.setBrightnessFromOffload(brightness); + DisplayPowerRequest dpr = new DisplayPowerRequest(); + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), + eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false)); + assertEquals(BrightnessReason.REASON_OFFLOAD, mHolder.dpc.mBrightnessReason.getReason()); + + // Now automatic brightness becomes available + brightness = 0.22f; + when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness( + any(BrightnessEvent.class))).thenReturn(brightness); + + mHolder.dpc.updateBrightness(); + advanceTime(1); // Run updatePowerState + + verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), + eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false)); + assertEquals(BrightnessReason.REASON_AUTOMATIC, mHolder.dpc.mBrightnessReason.getReason()); } @Test 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 b99ecf3978ae..14de527aa1f7 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -46,7 +46,6 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.PowerManager; import android.view.Display; import android.view.DisplayAddress; import android.view.SurfaceControl; @@ -1229,8 +1228,6 @@ public class LocalDisplayAdapterTest { verify(mDisplayOffloader).stopOffload(); assertFalse(mDisplayOffloadSession.isActive()); - verify(mMockedDisplayPowerController).setBrightnessFromOffload( - PowerManager.BRIGHTNESS_INVALID_FLOAT); } private void initDisplayOffloadSession() { diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java index 289d54b86c7d..9b6cc0a4b377 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java @@ -393,7 +393,31 @@ public final class DisplayBrightnessControllerTest { OffloadBrightnessStrategy offloadBrightnessStrategy = mock(OffloadBrightnessStrategy.class); when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn( offloadBrightnessStrategy); - mDisplayBrightnessController.setBrightnessFromOffload(brightness); + boolean brightnessUpdated = + mDisplayBrightnessController.setBrightnessFromOffload(brightness); verify(offloadBrightnessStrategy).setOffloadScreenBrightness(brightness); + assertTrue(brightnessUpdated); + } + + @Test + public void setBrightnessFromOffload_OffloadStrategyNull() { + float brightness = 0.4f; + when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn(null); + boolean brightnessUpdated = + mDisplayBrightnessController.setBrightnessFromOffload(brightness); + assertFalse(brightnessUpdated); + } + + @Test + public void setBrightnessFromOffload_BrightnessUnchanged() { + float brightness = 0.4f; + OffloadBrightnessStrategy offloadBrightnessStrategy = mock(OffloadBrightnessStrategy.class); + when(offloadBrightnessStrategy.getOffloadScreenBrightness()).thenReturn(brightness); + when(mDisplayBrightnessStrategySelector.getOffloadBrightnessStrategy()).thenReturn( + offloadBrightnessStrategy); + boolean brightnessUpdated = + mDisplayBrightnessController.setBrightnessFromOffload(brightness); + verify(offloadBrightnessStrategy, never()).setOffloadScreenBrightness(brightness); + assertFalse(brightnessUpdated); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java index 1c681ce21f02..0e89d8383a8f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java @@ -247,6 +247,7 @@ public final class DisplayBrightnessStrategySelectorTest { displayPowerRequest.screenBrightnessOverride = Float.NaN; when(mFollowerBrightnessStrategy.getBrightnessToFollow()).thenReturn(Float.NaN); when(mTemporaryBrightnessStrategy.getTemporaryScreenBrightness()).thenReturn(Float.NaN); + when(mAutomaticBrightnessStrategy.shouldUseAutoBrightness()).thenReturn(true); when(mOffloadBrightnessStrategy.getOffloadScreenBrightness()).thenReturn(0.3f); assertEquals(mOffloadBrightnessStrategy, mDisplayBrightnessStrategySelector.selectStrategy( displayPowerRequest, Display.STATE_ON)); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java index ba462e363b4e..a5dc668944df 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java @@ -188,29 +188,6 @@ public class AutomaticBrightnessStrategyTest { } @Test - public void testAutoBrightnessState_BrightnessReasonIsOffload() { - mAutomaticBrightnessStrategy.setUseAutoBrightness(true); - int targetDisplayState = Display.STATE_ON; - boolean allowAutoBrightnessWhileDozing = false; - int brightnessReason = BrightnessReason.REASON_OFFLOAD; - int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; - float lastUserSetBrightness = 0.2f; - boolean userSetBrightnessChanged = true; - mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(); - mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState, - allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness, - userSetBrightnessChanged); - verify(mAutomaticBrightnessController) - .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, - mBrightnessConfiguration, - lastUserSetBrightness, - userSetBrightnessChanged, 0.5f, - false, policy, true); - assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()); - assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff()); - } - - @Test public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() { mAutomaticBrightnessStrategy.setUseAutoBrightness(true); int targetDisplayState = Display.STATE_DOZE; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 3f6117be6143..e141faf599fa 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -2001,6 +2001,59 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { } @Test + public void testReplacePendingToCachedProcess_withDeferrableBroadcast() throws Exception { + // Legacy stack doesn't support deferral + Assume.assumeTrue(mImpl == Impl.MODERN); + + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); + final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE); + final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); + + setProcessFreezable(receiverGreenApp, true, false); + mQueue.onProcessFreezableChangedLocked(receiverGreenApp); + waitForIdle(); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK) + .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + final BroadcastOptions opts = BroadcastOptions.makeBasic() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE); + + final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 10); + final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 5); + final BroadcastFilter receiverYellow = makeRegisteredReceiver(receiverYellowApp, 0); + enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, opts, List.of( + receiverGreen, receiverBlue, receiverYellow))); + + // Enqueue the broadcast again to replace the earlier one + enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, opts, List.of( + receiverGreen, receiverBlue, receiverYellow))); + + waitForIdle(); + // Green should still be in the cached state and shouldn't receive the broadcast + verifyScheduleRegisteredReceiver(never(), receiverGreenApp, timeTick); + + final IApplicationThread blueThread = receiverBlueApp.getThread(); + final IApplicationThread yellowThread = receiverYellowApp.getThread(); + final InOrder inOrder = inOrder(blueThread, yellowThread); + inOrder.verify(blueThread).scheduleRegisteredReceiver( + any(), argThat(filterEqualsIgnoringComponent(timeTick)), + anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(), + eq(UserHandle.USER_SYSTEM), anyInt(), anyInt(), any()); + inOrder.verify(yellowThread).scheduleRegisteredReceiver( + any(), argThat(filterEqualsIgnoringComponent(timeTick)), + anyInt(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(), + eq(UserHandle.USER_SYSTEM), anyInt(), anyInt(), any()); + + setProcessFreezable(receiverGreenApp, false, false); + mQueue.onProcessFreezableChangedLocked(receiverGreenApp); + waitForIdle(); + + // Confirm that green receives the broadcast once it comes out of the cached state + verifyScheduleRegisteredReceiver(times(1), receiverGreenApp, timeTick); + } + + @Test public void testIdleAndBarrier() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index 6bcd778c234b..c6a6865f1cf1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -60,10 +60,13 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.app.AlarmManager; import android.app.AppGlobals; import android.app.job.JobInfo; @@ -71,12 +74,15 @@ import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.PackageManagerInternal; import android.net.NetworkRequest; import android.os.Looper; import android.os.PowerManager; +import android.os.UserHandle; import android.provider.DeviceConfig; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotMapping; import android.util.ArraySet; import android.util.EmptyArray; import android.util.SparseArray; @@ -104,6 +110,9 @@ import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.Executor; public class FlexibilityControllerTest { @@ -113,6 +122,9 @@ public class FlexibilityControllerTest { private MockitoSession mMockingSession; private BroadcastReceiver mBroadcastReceiver; + private final SparseArray<ArraySet<String>> mCarrierPrivilegedApps = new SparseArray<>(); + private final SparseArray<TelephonyManager.CarrierPrivilegesCallback> + mCarrierPrivilegedCallbacks = new SparseArray<>(); private FlexibilityController mFlexibilityController; private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; private JobStore mJobStore; @@ -130,6 +142,10 @@ public class FlexibilityControllerTest { @Mock private PrefetchController mPrefetchController; @Mock + private TelephonyManager mTelephonyManager; + @Mock + private IPackageManager mIPackageManager; + @Mock private PackageManager mPackageManager; @Before @@ -138,6 +154,7 @@ public class FlexibilityControllerTest { .initMocks(this) .strictness(Strictness.LENIENT) .spyStatic(DeviceConfig.class) + .mockStatic(AppGlobals.class) .mockStatic(LocalServices.class) .startMocking(); // Called in StateController constructor. @@ -167,17 +184,23 @@ public class FlexibilityControllerTest { -> mDeviceConfigPropertiesBuilder.build()) .when(() -> DeviceConfig.getProperties( eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any())); + // Used in FlexibilityController.SpecialAppTracker. + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) + .thenReturn(true); //used to get jobs by UID mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir()); doReturn(mJobStore).when(mJobSchedulerService).getJobStore(); // Used in JobStatus. - doReturn(mock(PackageManagerInternal.class)) - .when(() -> LocalServices.getService(PackageManagerInternal.class)); + doReturn(mIPackageManager).when(AppGlobals::getPackageManager); // Freeze the clocks at a moment in time JobSchedulerService.sSystemClock = Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC); + // Set empty set of privileged apps. + setSimSlotMappings(null); + setPowerWhitelistExceptIdle(); // Initialize real objects. doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(any()); ArgumentCaptor<BroadcastReceiver> receiverCaptor = @@ -249,9 +272,13 @@ public class FlexibilityControllerTest { } private JobStatus createJobStatus(String testTag, JobInfo.Builder job) { + return createJobStatus(testTag, job, SOURCE_PACKAGE); + } + + private JobStatus createJobStatus(String testTag, JobInfo.Builder job, String sourcePackage) { JobInfo jobInfo = job.build(); JobStatus js = JobStatus.createFromJobInfo( - jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag); + jobInfo, 1000, sourcePackage, SOURCE_USER_ID, "FCTest", testTag); js.enqueueTime = FROZEN_TIME; js.setStandbyBucket(ACTIVE_INDEX); if (js.hasFlexibilityConstraint()) { @@ -1084,7 +1111,6 @@ public class FlexibilityControllerTest { @Test public void testAllowlistedAppBypass() { - setPowerWhitelistExceptIdle(); mFlexibilityController.onSystemServicesReady(); JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass", @@ -1118,6 +1144,148 @@ public class FlexibilityControllerTest { } @Test + public void testCarrierPrivilegedAppBypass() throws Exception { + mFlexibilityController.onSystemServicesReady(); + + final String carrier1Pkg1 = "com.test.carrier.1.pkg.1"; + final String carrier1Pkg2 = "com.test.carrier.1.pkg.2"; + final String carrier2Pkg = "com.test.carrier.2.pkg"; + final String nonCarrierPkg = "com.test.normal.pkg"; + + setPackageUid(carrier1Pkg1, 1); + setPackageUid(carrier1Pkg2, 11); + setPackageUid(carrier2Pkg, 2); + setPackageUid(nonCarrierPkg, 3); + + // Set the second carrier's privileged list before SIM configuration is sent to test + // initialization. + setCarrierPrivilegedAppList(2, carrier2Pkg); + + UiccSlotMapping sim1 = mock(UiccSlotMapping.class); + UiccSlotMapping sim2 = mock(UiccSlotMapping.class); + doReturn(1).when(sim1).getLogicalSlotIndex(); + doReturn(2).when(sim2).getLogicalSlotIndex(); + setSimSlotMappings(List.of(sim1, sim2)); + + JobStatus jsHighC1P1 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier1Pkg1); + JobStatus jsDefaultC1P1 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier1Pkg1); + JobStatus jsLowC1P1 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier1Pkg1); + JobStatus jsMinC1P1 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier1Pkg1); + JobStatus jsHighC1P2 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier1Pkg2); + JobStatus jsDefaultC1P2 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier1Pkg2); + JobStatus jsLowC1P2 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier1Pkg2); + JobStatus jsMinC1P2 = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier1Pkg2); + JobStatus jsHighC2P = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH), carrier2Pkg); + JobStatus jsDefaultC2P = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), carrier2Pkg); + JobStatus jsLowC2P = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_LOW), carrier2Pkg); + JobStatus jsMinC2P = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_MIN), carrier2Pkg); + JobStatus jsHighNCP = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH), nonCarrierPkg); + JobStatus jsDefaultNCP = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT), nonCarrierPkg); + JobStatus jsLowNCP = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_LOW), nonCarrierPkg); + JobStatus jsMinNCP = createJobStatus("testCarrierPrivilegedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_MIN), nonCarrierPkg); + + setCarrierPrivilegedAppList(1); + synchronized (mFlexibilityController.mLock) { + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP)); + } + + // Only mark the first package of carrier 1 as privileged. Only that app's jobs should + // be exempted. + setCarrierPrivilegedAppList(1, carrier1Pkg1); + synchronized (mFlexibilityController.mLock) { + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP)); + } + + // Add the second package of carrier 1. Both apps' jobs should be exempted. + setCarrierPrivilegedAppList(1, carrier1Pkg1, carrier1Pkg2); + synchronized (mFlexibilityController.mLock) { + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP)); + } + + // Remove a SIM slot. The relevant app's should no longer have exempted jobs. + setSimSlotMappings(List.of(sim1)); + synchronized (mFlexibilityController.mLock) { + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P1)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P1)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P1)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC1P2)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC1P2)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinC2P)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHighNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefaultNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLowNCP)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMinNCP)); + } + } + + @Test public void testForegroundAppBypass() { JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)); @@ -1753,6 +1921,24 @@ public class FlexibilityControllerTest { } } + private void setCarrierPrivilegedAppList(int logicalIndex, String... packages) { + final ArraySet<String> packageSet = packages == null + ? new ArraySet<>() : new ArraySet<>(packages); + mCarrierPrivilegedApps.put(logicalIndex, packageSet); + + TelephonyManager.CarrierPrivilegesCallback callback = + mCarrierPrivilegedCallbacks.get(logicalIndex); + if (callback != null) { + callback.onCarrierPrivilegesChanged(packageSet, Collections.emptySet()); + waitForQuietModuleThread(); + } + } + + private void setPackageUid(final String pkgName, final int uid) throws Exception { + doReturn(uid).when(mIPackageManager) + .getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid))); + } + private void setPowerWhitelistExceptIdle(String... packages) { doReturn(packages == null ? EmptyArray.STRING : packages) .when(mDeviceIdleInternal).getFullPowerWhitelistExceptIdle(); @@ -1763,6 +1949,47 @@ public class FlexibilityControllerTest { } } + private void setSimSlotMappings(@Nullable Collection<UiccSlotMapping> simSlotMapping) { + clearInvocations(mTelephonyManager); + final Collection<UiccSlotMapping> returnedMapping = simSlotMapping == null + ? Collections.emptyList() : simSlotMapping; + doReturn(returnedMapping).when(mTelephonyManager).getSimSlotMapping(); + if (mBroadcastReceiver != null) { + final Intent intent = new Intent(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); + mBroadcastReceiver.onReceive(mContext, intent); + waitForQuietModuleThread(); + } + if (returnedMapping.size() > 0) { + ArgumentCaptor<TelephonyManager.CarrierPrivilegesCallback> callbackCaptor = + ArgumentCaptor.forClass(TelephonyManager.CarrierPrivilegesCallback.class); + ArgumentCaptor<Integer> logicalIndexCaptor = ArgumentCaptor.forClass(Integer.class); + + final int minExpectedNewRegistrations = Math.max(0, + returnedMapping.size() - mCarrierPrivilegedCallbacks.size()); + verify(mTelephonyManager, atLeast(minExpectedNewRegistrations)) + .registerCarrierPrivilegesCallback( + logicalIndexCaptor.capture(), any(), callbackCaptor.capture()); + + final List<Integer> registeredIndices = logicalIndexCaptor.getAllValues(); + final List<TelephonyManager.CarrierPrivilegesCallback> registeredCallbacks = + callbackCaptor.getAllValues(); + for (int i = 0; i < registeredIndices.size(); ++i) { + final int logicalIndex = registeredIndices.get(i); + final TelephonyManager.CarrierPrivilegesCallback callback = + registeredCallbacks.get(i); + + mCarrierPrivilegedCallbacks.put(logicalIndex, callback); + + // The API contract promises a callback upon registration with the current list. + final ArraySet<String> cpApps = mCarrierPrivilegedApps.get(logicalIndex); + callback.onCarrierPrivilegesChanged( + cpApps == null ? Collections.emptySet() : cpApps, + Collections.emptySet()); + } + waitForQuietModuleThread(); + } + } + private void setUidBias(int uid, int bias) { int prevBias = mJobSchedulerService.getUidBias(uid); doReturn(bias).when(mJobSchedulerService).getUidBias(uid); diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp new file mode 100644 index 000000000000..e94b8ad0a9ac --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp @@ -0,0 +1,56 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "RollbackPackageHealthObserverTests", + + srcs: [ + "*.java", + ], + + static_libs: [ + "androidx.test.runner", + "mockito-target-extended-minus-junit4", + "services.core", + "truth", + "flag-junit", + ], + + libs: [ + "android.test.mock", + "android.test.base", + "android.test.runner", + ], + + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + certificate: "platform", + platform_apis: true, + test_suites: [ + "device-tests", + "automotive-tests", + ], +} diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidManifest.xml b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidManifest.xml new file mode 100644 index 000000000000..c52dbdee4b4b --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.rollback"> + + <uses-sdk android:targetSdkVersion="35" /> + + <application android:testOnly="true" + android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.rollback" + android:label="Frameworks Rollback Package Health Observer test" /> +</manifest> diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidTest.xml b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidTest.xml new file mode 100644 index 000000000000..635183c553bf --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/rollback/AndroidTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="Runs Rollback Package Health Observer Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="RollbackPackageHealthObserverTests.apk" /> + </target_preparer> + + <option name="test-tag" value="RollbackPackageHealthObserverTests" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.server.rollback" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING b/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING index e42bdad97730..6ac56bfc254a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING +++ b/services/tests/mockingservicestests/src/com/android/server/rollback/TEST_MAPPING @@ -1,7 +1,7 @@ { "postsubmit": [ { - "name": "FrameworksMockingServicesTests", + "name": "RollbackPackageHealthObserverTests", "options": [ { "include-filter": "com.android.server.rollback" diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java new file mode 100644 index 000000000000..7ecc7fd1b94b --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wallpaper; + +import static android.app.WallpaperManager.LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; +import static android.app.WallpaperManager.PORTRAIT; +import static android.app.WallpaperManager.SQUARE_LANDSCAPE; +import static android.app.WallpaperManager.SQUARE_PORTRAIT; +import static android.app.WallpaperManager.getOrientation; +import static android.app.WallpaperManager.getRotatedOrientation; + +import static com.android.window.flags.Flags.FLAG_MULTI_CROP; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.util.SparseArray; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.Comparator; +import java.util.List; + +/** + * Unit tests for the most important helpers of {@link WallpaperCropper}, in particular + * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}. + */ +@Presubmit +@RunWith(AndroidJUnit4.class) +@RequiresFlagsEnabled(FLAG_MULTI_CROP) +public class WallpaperCropperTest { + + @Mock + private WallpaperDisplayHelper mWallpaperDisplayHelper; + private WallpaperCropper mWallpaperCropper; + + private static final Point PORTRAIT_ONE = new Point(500, 800); + private static final Point PORTRAIT_TWO = new Point(400, 1000); + private static final Point PORTRAIT_THREE = new Point(2000, 800); + private static final Point PORTRAIT_FOUR = new Point(1600, 1000); + + private static final Point SQUARE_PORTRAIT_ONE = new Point(1000, 800); + private static final Point SQUARE_LANDSCAPE_ONE = new Point(800, 1000); + + /** + * Common device: a single screen of portrait/landscape orientation + */ + private static final List<Point> STANDARD_DISPLAY = List.of(PORTRAIT_ONE); + + /** 1: folded: portrait, unfolded: square with w < h */ + private static final List<Point> FOLDABLE_ONE = List.of(PORTRAIT_ONE, SQUARE_PORTRAIT_ONE); + + /** 2: folded: portrait, unfolded: square with w > h */ + private static final List<Point> FOLDABLE_TWO = List.of(PORTRAIT_TWO, SQUARE_LANDSCAPE_ONE); + + /** 3: folded: square with w < h, unfolded: portrait */ + private static final List<Point> FOLDABLE_THREE = List.of(SQUARE_PORTRAIT_ONE, PORTRAIT_THREE); + + /** 4: folded: square with w > h, unfolded: portrait */ + private static final List<Point> FOLDABLE_FOUR = List.of(SQUARE_LANDSCAPE_ONE, PORTRAIT_FOUR); + + /** + * List of different sets of displays for foldable devices. Foldable devices have two displays: + * a folded (smaller) unfolded (larger). + */ + private static final List<List<Point>> ALL_FOLDABLE_DISPLAYS = List.of( + FOLDABLE_ONE, FOLDABLE_TWO, FOLDABLE_THREE, FOLDABLE_FOUR); + + private SparseArray<Point> mDisplaySizes = new SparseArray<>(); + private int mFolded = ORIENTATION_UNKNOWN; + private int mFoldedRotated = ORIENTATION_UNKNOWN; + private int mUnfolded = ORIENTATION_UNKNOWN; + private int mUnfoldedRotated = ORIENTATION_UNKNOWN; + + private static final List<Integer> ALL_MODES = List.of( + WallpaperCropper.ADD, WallpaperCropper.REMOVE, WallpaperCropper.BALANCE); + + @Before + public void setUp() { + initMocks(this); + mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); + } + + private void setUpWithDisplays(List<Point> displaySizes) { + mDisplaySizes = new SparseArray<>(); + displaySizes.forEach(size -> { + mDisplaySizes.put(getOrientation(size), size); + Point rotated = new Point(size.y, size.x); + mDisplaySizes.put(getOrientation(rotated), rotated); + }); + when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes); + if (displaySizes.size() == 2) { + Point largestDisplay = displaySizes.stream().max( + Comparator.comparingInt(p -> p.x * p.y)).get(); + Point smallestDisplay = displaySizes.stream().min( + Comparator.comparingInt(p -> p.x * p.y)).get(); + mUnfolded = getOrientation(largestDisplay); + mFolded = getOrientation(smallestDisplay); + mUnfoldedRotated = getRotatedOrientation(mUnfolded); + mFoldedRotated = getRotatedOrientation(mFolded); + } + doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0))) + .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt()); + doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0))) + .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt()); + } + + private int getFoldedOrientation(int orientation) { + if (orientation == ORIENTATION_UNKNOWN) return ORIENTATION_UNKNOWN; + if (orientation == mUnfolded) return mFolded; + if (orientation == mUnfoldedRotated) return mFoldedRotated; + return ORIENTATION_UNKNOWN; + } + + private int getUnfoldedOrientation(int orientation) { + if (orientation == ORIENTATION_UNKNOWN) return ORIENTATION_UNKNOWN; + if (orientation == mFolded) return mUnfolded; + if (orientation == mFoldedRotated) return mUnfoldedRotated; + return ORIENTATION_UNKNOWN; + } + + /** + * Test that {@link WallpaperCropper#noParallax} successfully removes the parallax in a simple + * case, removing the right or left part depending on the "rtl" argument. + */ + @Test + public void testNoParallax_noScale() { + Point displaySize = new Point(1000, 1000); + Point bitmapSize = new Point(1200, 1000); + Point expectedCropSize = new Point(1000, 1000); + Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ false)) + .isEqualTo(leftOf(crop, expectedCropSize)); + assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ true)) + .isEqualTo(rightOf(crop, expectedCropSize)); + } + + /** + * Test that {@link WallpaperCropper#noParallax} correctly takes zooming into account. + */ + @Test + public void testNoParallax_withScale() { + Point displaySize = new Point(1000, 1000); + Point bitmapSize = new Point(600, 500); + Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + Point expectedCropSize = new Point(500, 500); + assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ false)) + .isEqualTo(leftOf(crop, expectedCropSize)); + assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ true)) + .isEqualTo(rightOf(crop, expectedCropSize)); + } + + /** + * Test that {@link WallpaperCropper#noParallax} correctly removes parallax when the image is + * cropped, i.e. when the crop rectangle is not the full bitmap. + */ + @Test + public void testNoParallax_withScaleAndCrop() { + Point displaySize = new Point(1000, 1000); + Point bitmapSize = new Point(2000, 2000); + Rect crop = new Rect(300, 1000, 900, 1500); + Point expectedCropSize = new Point(500, 500); + assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ false)) + .isEqualTo(leftOf(crop, expectedCropSize)); + assertThat(WallpaperCropper.noParallax(crop, displaySize, bitmapSize, /* rtl */ true)) + .isEqualTo(rightOf(crop, expectedCropSize)); + } + + /** + * Test that {@link WallpaperCropper#getAdjustedCrop} does nothing when the crop has the same + * width/height ratio than the screen. + */ + @Test + public void testGetAdjustedCrop_noOp() { + Point displaySize = new Point(1000, 1000); + + for (Point bitmapSize: List.of( + new Point(1000, 1000), + new Point(2000, 2000), + new Point(500, 500))) { + for (Rect crop: List.of( + new Rect(0, 0, bitmapSize.x, bitmapSize.y), + new Rect(100, 200, bitmapSize.x - 100, bitmapSize.y))) { + for (int mode: ALL_MODES) { + for (boolean rtl: List.of(true, false)) { + for (boolean parallax: List.of(true, false)) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, parallax, rtl, mode)) + .isEqualTo(crop); + } + } + } + } + } + } + + /** + * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with parallax = true, + * does not keep more width than needed for {@link WallpaperCropper#MAX_PARALLAX}. + */ + @Test + public void testGetAdjustedCrop_tooMuchParallax() { + Point displaySize = new Point(1000, 1000); + int tooLargeWidth = (int) (displaySize.x * (1 + 2 * WallpaperCropper.MAX_PARALLAX)); + Point bitmapSize = new Point(tooLargeWidth, 1000); + Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + int expectedWidth = (int) (displaySize.x * (1 + WallpaperCropper.MAX_PARALLAX)); + Point expectedCropSize = new Point(expectedWidth, 1000); + for (int mode: ALL_MODES) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, true, false, mode)) + .isEqualTo(leftOf(crop, expectedCropSize)); + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, true, true, mode)) + .isEqualTo(rightOf(crop, expectedCropSize)); + } + } + + /** + * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with parallax = true, + * does not remove parallax if the parallax is below {@link WallpaperCropper#MAX_PARALLAX}. + */ + @Test + public void testGetAdjustedCrop_acceptableParallax() { + Point displaySize = new Point(1000, 1000); + List<Integer> acceptableWidths = List.of(displaySize.x, + (int) (displaySize.x * (1 + 0.5 * WallpaperCropper.MAX_PARALLAX)), + (int) (displaySize.x * (1 + 0.9 * WallpaperCropper.MAX_PARALLAX)), + (int) (displaySize.x * (1 + 1.0 * WallpaperCropper.MAX_PARALLAX))); + for (int acceptableWidth: acceptableWidths) { + Point bitmapSize = new Point(acceptableWidth, 1000); + Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + for (int mode : ALL_MODES) { + for (boolean rtl : List.of(false, true)) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, true, rtl, mode)) + .isEqualTo(crop); + } + } + } + } + + /** + * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with + * {@link WallpaperCropper#ADD}, correctly enlarges the crop to match the display dimensions, + * and adds content to the crop by an equal amount on both sides when possible. + */ + @Test + public void testGetAdjustedCrop_add() { + Point displaySize = new Point(1000, 1000); + Point bitmapSize = new Point(1000, 1000); + + List<Rect> crops = List.of( + new Rect(0, 0, 900, 1000), + new Rect(0, 0, 1000, 900), + new Rect(0, 0, 400, 500), + new Rect(500, 600, 1000, 1000)); + + List<Rect> expectedAdjustedCrops = List.of( + new Rect(0, 0, 1000, 1000), + new Rect(0, 0, 1000, 1000), + new Rect(0, 0, 500, 500), + new Rect(500, 500, 1000, 1000)); + + for (int i = 0; i < crops.size(); i++) { + Rect crop = crops.get(i); + Rect expectedCrop = expectedAdjustedCrops.get(i); + for (boolean rtl: List.of(false, true)) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, false, rtl, WallpaperCropper.ADD)) + .isEqualTo(expectedCrop); + } + } + } + + /** + * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with + * {@link WallpaperCropper#REMOVE}, correctly shrinks the crop to match the display dimensions, + * and removes content by an equal amount on both sides. + */ + @Test + public void testGetAdjustedCrop_remove() { + Point displaySize = new Point(1000, 1000); + Point bitmapSize = new Point(1500, 1500); + + List<Rect> crops = List.of( + new Rect(50, 0, 1150, 1000), + new Rect(0, 50, 1000, 1150)); + + Point expectedCropSize = new Point(1000, 1000); + + for (Rect crop: crops) { + for (boolean rtl : List.of(false, true)) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, false, rtl, WallpaperCropper.REMOVE)) + .isEqualTo(centerOf(crop, expectedCropSize)); + } + } + } + + /** + * Test that {@link WallpaperCropper#getAdjustedCrop}, when called with + * {@link WallpaperCropper#BALANCE}, gives an adjusted crop with the same center and same number + * of pixels when possible. + */ + @Test + public void testGetAdjustedCrop_balance() { + Point displaySize = new Point(500, 1000); + Point transposedDisplaySize = new Point(1000, 500); + Point bitmapSize = new Point(1000, 1000); + + List<Rect> crops = List.of( + new Rect(0, 250, 1000, 750), + new Rect(100, 0, 300, 100)); + + List<Rect> expectedAdjustedCrops = List.of( + new Rect(250, 0, 750, 1000), + new Rect(150, 0, 250, 200)); + + for (int i = 0; i < crops.size(); i++) { + Rect crop = crops.get(i); + Rect expected = expectedAdjustedCrops.get(i); + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, false, false, WallpaperCropper.BALANCE)) + .isEqualTo(expected); + + Rect transposedCrop = new Rect(crop.top, crop.left, crop.bottom, crop.right); + Rect expectedTransposed = new Rect( + expected.top, expected.left, expected.bottom, expected.right); + assertThat(WallpaperCropper.getAdjustedCrop(transposedCrop, bitmapSize, + transposedDisplaySize, false, false, WallpaperCropper.BALANCE)) + .isEqualTo(expectedTransposed); + } + } + + /** + * Test that {@link WallpaperCropper#getCrop} follows a simple centre-align strategy when + * no suggested crops are provided. + */ + @Test + public void testGetCrop_noSuggestedCrops_centersWallpaper() { + setUpWithDisplays(STANDARD_DISPLAY); + Point bitmapSize = new Point(800, 1000); + Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + + List<Point> displaySizes = List.of( + new Point(500, 1000), + new Point(1000, 500)); + List<Point> expectedCropSizes = List.of( + new Point(500, 1000), + new Point(800, 400)); + + for (int i = 0; i < displaySizes.size(); i++) { + Point displaySize = displaySizes.get(i); + Point expectedCropSize = expectedCropSizes.get(i); + for (boolean rtl : List.of(false, true)) { + assertThat(mWallpaperCropper.getCrop( + displaySize, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(centerOf(bitmapRect, expectedCropSize)); + } + } + } + + /** + * Test that {@link WallpaperCropper#getCrop} reuses a suggested crop of the same orientation + * as the display if possible, and does not remove additional width for parallax, + * but adds width if necessary. + */ + @Test + public void testGetCrop_hasSuggestedCrop() { + setUpWithDisplays(STANDARD_DISPLAY); + Point bitmapSize = new Point(800, 1000); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + suggestedCrops.put(PORTRAIT, new Rect(0, 0, 400, 800)); + for (int otherOrientation: List.of(LANDSCAPE, SQUARE_LANDSCAPE, SQUARE_PORTRAIT)) { + suggestedCrops.put(otherOrientation, new Rect(0, 0, 10, 10)); + } + + for (boolean rtl : List.of(false, true)) { + assertThat(mWallpaperCropper.getCrop( + new Point(300, 800), bitmapSize, suggestedCrops, rtl)) + .isEqualTo(suggestedCrops.get(PORTRAIT)); + assertThat(mWallpaperCropper.getCrop( + new Point(500, 800), bitmapSize, suggestedCrops, rtl)) + .isEqualTo(new Rect(0, 0, 500, 800)); + } + } + + /** + * Test that {@link WallpaperCropper#getCrop}, if there is no suggested crop of the same + * orientation as the display, reuses a suggested crop of the rotated orientation if possible, + * and preserves the center and number of pixels of the crop if possible. + * <p> + * To simplify, in this test case all crops have the same size as the display (no zoom) + * and are at the center of the image. Also the image is large enough to preserver the number + * of pixels (no additional zoom required). + */ + @Test + public void testGetCrop_hasRotatedSuggestedCrop() { + setUpWithDisplays(STANDARD_DISPLAY); + Point bitmapSize = new Point(2000, 1800); + Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + Point portrait = PORTRAIT_ONE; + Point landscape = new Point(PORTRAIT_ONE.y, PORTRAIT_ONE.x); + Point squarePortrait = SQUARE_PORTRAIT_ONE; + Point squareLandscape = new Point(SQUARE_PORTRAIT_ONE.y, SQUARE_PORTRAIT_ONE.y); + suggestedCrops.put(PORTRAIT, centerOf(bitmapRect, portrait)); + suggestedCrops.put(SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape)); + for (boolean rtl : List.of(false, true)) { + assertThat(mWallpaperCropper.getCrop( + landscape, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(centerOf(bitmapRect, landscape)); + + assertThat(mWallpaperCropper.getCrop( + squarePortrait, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(centerOf(bitmapRect, squarePortrait)); + } + } + + /** + * Test that {@link WallpaperCropper#getCrop}, when asked for a folded crop with a suggested + * crop only for the relative unfolded orientation, creates the folded crop at the center of the + * unfolded crop, by removing content on two sides to match the folded screen dimensions. + * <p> + * To simplify, in this test case all crops have the same size as the display (no zoom) + * and are at the center of the image. + */ + @Test + public void testGetCrop_hasUnfoldedSuggestedCrop() { + for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { + setUpWithDisplays(displaySizes); + Point bitmapSize = new Point(2000, 2400); + Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + + Point largestDisplay = displaySizes.stream().max( + Comparator.comparingInt(a -> a.x * a.y)).orElseThrow(); + int unfoldedOne = getOrientation(largestDisplay); + int unfoldedTwo = getRotatedOrientation(unfoldedOne); + Rect unfoldedCropOne = centerOf(bitmapRect, mDisplaySizes.get(unfoldedOne)); + Rect unfoldedCropTwo = centerOf(bitmapRect, mDisplaySizes.get(unfoldedTwo)); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + suggestedCrops.put(unfoldedOne, unfoldedCropOne); + suggestedCrops.put(unfoldedTwo, unfoldedCropTwo); + + int foldedOne = getFoldedOrientation(unfoldedOne); + int foldedTwo = getFoldedOrientation(unfoldedTwo); + Point foldedDisplayOne = mDisplaySizes.get(foldedOne); + Point foldedDisplayTwo = mDisplaySizes.get(foldedTwo); + + for (boolean rtl : List.of(false, true)) { + assertThat(mWallpaperCropper.getCrop( + foldedDisplayOne, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(centerOf(unfoldedCropOne, foldedDisplayOne)); + + assertThat(mWallpaperCropper.getCrop( + foldedDisplayTwo, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(centerOf(unfoldedCropTwo, foldedDisplayTwo)); + } + } + } + + /** + * Test that {@link WallpaperCropper#getCrop}, when asked for an unfolded crop with a suggested + * crop only for the relative folded orientation, creates the unfolded crop with the same center + * as the folded crop, by adding content on two sides to match the unfolded screen dimensions. + * <p> + * To simplify, in this test case all crops have the same size as the display (no zoom) and are + * at the center of the image. Also the image is large enough to add content. + */ + @Test + public void testGetCrop_hasFoldedSuggestedCrop() { + for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { + setUpWithDisplays(displaySizes); + Point bitmapSize = new Point(2000, 2000); + Rect bitmapRect = new Rect(0, 0, 2000, 2000); + + Point smallestDisplay = displaySizes.stream().min( + Comparator.comparingInt(a -> a.x * a.y)).orElseThrow(); + int foldedOne = getOrientation(smallestDisplay); + int foldedTwo = getRotatedOrientation(foldedOne); + Point foldedDisplayOne = mDisplaySizes.get(foldedOne); + Point foldedDisplayTwo = mDisplaySizes.get(foldedTwo); + Rect foldedCropOne = centerOf(bitmapRect, foldedDisplayOne); + Rect foldedCropTwo = centerOf(bitmapRect, foldedDisplayTwo); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + suggestedCrops.put(foldedOne, foldedCropOne); + suggestedCrops.put(foldedTwo, foldedCropTwo); + + int unfoldedOne = getUnfoldedOrientation(foldedOne); + int unfoldedTwo = getUnfoldedOrientation(foldedTwo); + Point unfoldedDisplayOne = mDisplaySizes.get(unfoldedOne); + Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo); + + for (boolean rtl : List.of(false, true)) { + assertThat(centerOf(mWallpaperCropper.getCrop( + unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne)) + .isEqualTo(foldedCropOne); + + assertThat(centerOf(mWallpaperCropper.getCrop( + unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo)) + .isEqualTo(foldedCropTwo); + } + } + } + + /** + * Test that {@link WallpaperCropper#getCrop}, when asked for an folded crop with a suggested + * crop only for the rotated unfolded orientation, creates the folded crop from that crop by + * combining a rotate + fold operation. The folded crop should have less pixels than the + * unfolded crop due to the fold operation which removes content on both sides of the image. + * <p> + * To simplify, in this test case all crops have the same size as the display (no zoom) + * and are at the center of the image. + */ + @Test + public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() { + for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { + setUpWithDisplays(displaySizes); + Point bitmapSize = new Point(2000, 2000); + Rect bitmapRect = new Rect(0, 0, 2000, 2000); + Point largestDisplay = displaySizes.stream().max( + Comparator.comparingInt(a -> a.x * a.y)).orElseThrow(); + int unfoldedOne = getOrientation(largestDisplay); + int unfoldedTwo = getRotatedOrientation(unfoldedOne); + for (int unfolded: List.of(unfoldedOne, unfoldedTwo)) { + Rect unfoldedCrop = centerOf(bitmapRect, mDisplaySizes.get(unfolded)); + int rotatedUnfolded = getRotatedOrientation(unfolded); + Rect rotatedUnfoldedCrop = centerOf(bitmapRect, mDisplaySizes.get(rotatedUnfolded)); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + suggestedCrops.put(unfolded, unfoldedCrop); + int rotatedFolded = getFoldedOrientation(rotatedUnfolded); + Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded); + + for (boolean rtl : List.of(false, true)) { + assertThat(mWallpaperCropper.getCrop( + rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay)); + } + } + } + } + + /** + * Test that {@link WallpaperCropper#getCrop}, when asked for an unfolded crop with a suggested + * crop only for the rotated folded orientation, creates the unfolded crop from that crop by + * combining a rotate + unfold operation. The unfolded crop should have more pixels than the + * folded crop due to the unfold operation which adds content on two sides of the image. + * <p> + * To simplify, in this test case all crops have the same size as the display (no zoom) + * and are centered inside the image. Also the image is large enough to add content. + */ + @Test + public void testGetCrop_hasRotatedFoldedSuggestedCrop() { + for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { + setUpWithDisplays(displaySizes); + Point bitmapSize = new Point(2000, 2000); + Rect bitmapRect = new Rect(0, 0, 2000, 2000); + + Point smallestDisplay = displaySizes.stream().min( + Comparator.comparingInt(a -> a.x * a.y)).orElseThrow(); + int foldedOne = getOrientation(smallestDisplay); + int foldedTwo = getRotatedOrientation(foldedOne); + for (int folded: List.of(foldedOne, foldedTwo)) { + Rect foldedCrop = centerOf(bitmapRect, mDisplaySizes.get(folded)); + SparseArray<Rect> suggestedCrops = new SparseArray<>(); + suggestedCrops.put(folded, foldedCrop); + int rotatedFolded = getRotatedOrientation(folded); + int rotatedUnfolded = getUnfoldedOrientation(rotatedFolded); + Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded); + Rect rotatedFoldedCrop = centerOf(bitmapRect, rotatedFoldedDisplay); + Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded); + + for (boolean rtl : List.of(false, true)) { + Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop( + rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl); + assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay)) + .isEqualTo(rotatedFoldedCrop); + } + } + } + } + + private static Rect centerOf(Rect container, Point point) { + checkSubset(container, point); + int diffWidth = container.width() - point.x; + int diffHeight = container.height() - point.y; + int startX = container.left + diffWidth / 2; + int startY = container.top + diffHeight / 2; + return new Rect(startX, startY, startX + point.x, startY + point.y); + } + + private static Rect leftOf(Rect container, Point point) { + Rect result = centerOf(container, point); + result.offset(container.left - result.left, 0); + return result; + } + + private static Rect rightOf(Rect container, Point point) { + checkSubset(container, point); + Rect result = centerOf(container, point); + result.offset(container.right - result.right, 0); + return result; + } + + private static void checkSubset(Rect container, Point point) { + if (container.width() < point.x || container.height() < point.y) { + throw new IllegalArgumentException(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java index 4db27d272f8e..dc26e6e2374c 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java @@ -17,9 +17,9 @@ package com.android.server.accessibility; import static com.android.server.accessibility.AbstractAccessibilityServiceConnection.DISPLAY_TYPE_DEFAULT; -import static com.android.server.accessibility.AccessibilityWindowManagerTest.DisplayIdMatcher.displayId; -import static com.android.server.accessibility.AccessibilityWindowManagerTest.WindowChangesMatcher.a11yWindowChanges; -import static com.android.server.accessibility.AccessibilityWindowManagerTest.WindowIdMatcher.a11yWindowId; +import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.DisplayIdMatcher.displayId; +import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowChangesMatcher.a11yWindowChanges; +import static com.android.server.accessibility.AccessibilityWindowManagerWithAccessibilityWindowTest.WindowIdMatcher.a11yWindowId; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -27,11 +27,13 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -42,6 +44,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.graphics.Point; import android.graphics.Region; import android.os.IBinder; import android.os.LocaleList; @@ -63,6 +66,7 @@ import android.view.accessibility.IAccessibilityInteractionConnection; import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection; import com.android.server.accessibility.test.MessageCapturingHandler; +import com.android.server.wm.AccessibilityWindowsPopulator.AccessibilityWindow; import com.android.server.wm.WindowManagerInternal; import com.android.server.wm.WindowManagerInternal.WindowsForAccessibilityCallback; @@ -117,9 +121,8 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { // List of window token, mapping from windowId -> window token. private final SparseArray<IWindow> mA11yWindowTokens = new SparseArray<>(); - // List of window info lists, mapping from displayId -> window info lists. - private final SparseArray<ArrayList<WindowInfo>> mWindowInfos = - new SparseArray<>(); + // List of window info lists, mapping from displayId -> a11y window lists. + private final SparseArray<ArrayList<AccessibilityWindow>> mWindows = new SparseArray<>(); // List of callback, mapping from displayId -> callback. private final SparseArray<WindowsForAccessibilityCallback> mCallbackOfWindows = new SparseArray<>(); @@ -129,6 +132,13 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { private final MessageCapturingHandler mHandler = new MessageCapturingHandler(null); + // This maps displayId -> next region offset. + // Touchable region must have un-occluded area so that it's exposed to a11y services. + // This offset can be used as left and top of new region so that top-left of each region are + // kept visible. + // It's expected to be incremented by some amount everytime the value is used. + private final SparseArray<Integer> mNextRegionOffsets = new SparseArray<>(); + @Mock private WindowManagerInternal mMockWindowManagerInternal; @Mock @@ -162,7 +172,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { anyString(), anyInt(), anyInt(), anyInt())).thenReturn(PACKAGE_NAME); doAnswer((invocation) -> { - onWindowsForAccessibilityChanged(invocation.getArgument(0), false); + onAccessibilityWindowsChanged(invocation.getArgument(0), false); return null; }).when(mMockWindowManagerInternal).computeWindowsForAccessibility(anyInt()); @@ -176,7 +186,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { // as top focused display before each testing starts. startTrackingPerDisplay(Display.DEFAULT_DISPLAY); - // AccessibilityEventSender is invoked during onWindowsForAccessibilityChanged. + // AccessibilityEventSender is invoked during onAccessibilityWindowsChanged. // Resets it for mockito verify of further test case. Mockito.reset(mMockA11yEventSender); @@ -240,19 +250,18 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void onWindowsChanged_duringTouchInteractAndFocusChange_shouldChangeActiveWindow() { final int activeWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); - WindowInfo focusedWindowInfo = - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX); + final WindowInfo focusedWindowInfo = + mWindows.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).getWindowInfo(); assertEquals(activeWindowId, mA11yWindowManager.findWindowIdLocked( USER_SYSTEM_ID, focusedWindowInfo.token)); focusedWindowInfo.focused = false; - focusedWindowInfo = - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX + 1); - focusedWindowInfo.focused = true; + mWindows.get(Display.DEFAULT_DISPLAY).get( + DEFAULT_FOCUSED_INDEX + 1).getWindowInfo().focused = true; mA11yWindowManager.onTouchInteractionStart(); setTopFocusedWindowAndDisplay(Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX + 1); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); assertNotEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID)); } @@ -276,7 +285,7 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { changeFocusedWindowOnDisplayPerDisplayFocusConfig(SECONDARY_DISPLAY_ID, DEFAULT_FOCUSED_INDEX + 1, Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX); - onWindowsForAccessibilityChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES); + onAccessibilityWindowsChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES); // The active window should not be changed. assertEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID)); // The top focused window should not be changed. @@ -304,8 +313,8 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { changeFocusedWindowOnDisplayPerDisplayFocusConfig(SECONDARY_DISPLAY_ID, DEFAULT_FOCUSED_INDEX, Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); - onWindowsForAccessibilityChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + onAccessibilityWindowsChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES); // The active window should be changed. assertNotEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID)); // The top focused window should be changed. @@ -315,53 +324,43 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void onWindowsChanged_shouldReportCorrectLayer() { - // AccessibilityWindowManager#onWindowsForAccessibilityChanged already invoked in setup. + // AccessibilityWindowManager#onAccessibilityWindowsChanged already invoked in setup. List<AccessibilityWindowInfo> a11yWindows = mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); for (int i = 0; i < a11yWindows.size(); i++) { final AccessibilityWindowInfo a11yWindow = a11yWindows.get(i); - final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(i); - assertThat(mWindowInfos.get(Display.DEFAULT_DISPLAY).size() - windowInfo.layer - 1, + assertThat(mWindows.get(Display.DEFAULT_DISPLAY).size() - i - 1, is(a11yWindow.getLayer())); } } @Test public void onWindowsChanged_shouldReportCorrectOrder() { - // AccessibilityWindowManager#onWindowsForAccessibilityChanged already invoked in setup. + // AccessibilityWindowManager#onAccessibilityWindowsChanged already invoked in setup. List<AccessibilityWindowInfo> a11yWindows = mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); for (int i = 0; i < a11yWindows.size(); i++) { final AccessibilityWindowInfo a11yWindow = a11yWindows.get(i); final IBinder windowToken = mA11yWindowManager .getWindowTokenForUserAndWindowIdLocked(USER_SYSTEM_ID, a11yWindow.getId()); - final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(i); + final WindowInfo windowInfo = mWindows.get(Display.DEFAULT_DISPLAY) + .get(i).getWindowInfo(); assertThat(windowToken, is(windowInfo.token)); } } @Test public void onWindowsChangedAndForceSend_shouldUpdateWindows() { - final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); - final int correctLayer = - mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer(); - windowInfo.layer += 1; + assertNotEquals("new title", + toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY) + .get(0).getTitle())); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); - assertNotEquals(correctLayer, - mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer()); - } + mWindows.get(Display.DEFAULT_DISPLAY).get(0).getWindowInfo().title = "new title"; - @Test - public void onWindowsChangedNoForceSend_layerChanged_shouldNotUpdateWindows() { - final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); - final int correctLayer = - mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer(); - windowInfo.layer += 1; - - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); - assertEquals(correctLayer, - mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer()); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + assertEquals("new title", + toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY) + .get(0).getTitle())); } @Test @@ -371,14 +370,10 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0); final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, true, USER_SYSTEM_ID); - final WindowInfo windowInfo = WindowInfo.obtain(); - windowInfo.type = AccessibilityWindowInfo.TYPE_APPLICATION; - windowInfo.token = token.asBinder(); - windowInfo.layer = 0; - windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - mWindowInfos.get(Display.DEFAULT_DISPLAY).set(0, windowInfo); + mWindows.get(Display.DEFAULT_DISPLAY).set(0, + createMockAccessibilityWindow(token, Display.DEFAULT_DISPLAY)); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); assertNotEquals(oldWindow, mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0)); } @@ -386,12 +381,12 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void onWindowsChangedNoForceSend_focusChanged_shouldUpdateWindows() { final WindowInfo focusedWindowInfo = - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX); - final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + mWindows.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).getWindowInfo(); + final WindowInfo windowInfo = mWindows.get(Display.DEFAULT_DISPLAY).get(0).getWindowInfo(); focusedWindowInfo.focused = false; windowInfo.focused = true; - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); assertTrue(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0) .isFocused()); } @@ -500,15 +495,18 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void computePartialInteractiveRegionForWindow_wholeVisible_returnWholeRegion() { // Updates top 2 z-order WindowInfo are whole visible. - WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); - windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2); - windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(1); - windowInfo.regionInScreen.set(0, SCREEN_HEIGHT / 2, - SCREEN_WIDTH, SCREEN_HEIGHT); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(firstWindow, + new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2)); + final AccessibilityWindow secondWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(1); + setRegionForMockAccessibilityWindow(secondWindow, + new Region(0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT)); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); final List<AccessibilityWindowInfo> a11yWindows = mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasSize(2)); final Region outBounds = new Region(); int windowId = a11yWindows.get(0).getId(); @@ -526,12 +524,17 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void computePartialInteractiveRegionForWindow_halfVisible_returnHalfRegion() { // Updates z-order #1 WindowInfo is half visible. - WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); - windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2); - - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(firstWindow, + new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2)); + final AccessibilityWindow secondWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(1); + setRegionForMockAccessibilityWindow(secondWindow, + new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); final List<AccessibilityWindowInfo> a11yWindows = mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasSize(2)); final Region outBounds = new Region(); int windowId = a11yWindows.get(1).getId(); @@ -542,9 +545,17 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void computePartialInteractiveRegionForWindow_notVisible_returnEmptyRegion() { - // Since z-order #0 WindowInfo is full screen, z-order #1 WindowInfo should be invisible. + // z-order #0 WindowInfo is full screen, z-order #1 WindowInfo should be invisible. + final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(firstWindow, + new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + final List<AccessibilityWindowInfo> a11yWindows = mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + // Note that the second window is also exposed even if region is empty because it's focused. + assertThat(a11yWindows, hasSize(2)); final Region outBounds = new Region(); int windowId = a11yWindows.get(1).getId(); @@ -555,16 +566,21 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void computePartialInteractiveRegionForWindow_partialVisible_returnVisibleRegion() { // Updates z-order #0 WindowInfo to have two interact-able areas. - Region region = new Region(0, 0, SCREEN_WIDTH, 200); + final Region region = new Region(0, 0, SCREEN_WIDTH, 200); region.op(0, SCREEN_HEIGHT - 200, SCREEN_WIDTH, SCREEN_HEIGHT, Region.Op.UNION); - WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); - windowInfo.regionInScreen.set(region); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + final AccessibilityWindow firstWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(firstWindow, region); + final AccessibilityWindow secondWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(1); + setRegionForMockAccessibilityWindow(secondWindow, + new Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); final List<AccessibilityWindowInfo> a11yWindows = mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasSize(2)); final Region outBounds = new Region(); - int windowId = a11yWindows.get(1).getId(); + final int windowId = a11yWindows.get(1).getId(); mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds); assertFalse(outBounds.getBounds().isEmpty()); @@ -575,7 +591,8 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void updateActiveAndA11yFocusedWindow_windowStateChangedEvent_noTracking_shouldUpdate() { final IBinder eventWindowToken = - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX + 1).token; + mWindows.get(Display.DEFAULT_DISPLAY) + .get(DEFAULT_FOCUSED_INDEX + 1).getWindowInfo().token; final int eventWindowId = mA11yWindowManager.findWindowIdLocked( USER_SYSTEM_ID, eventWindowToken); when(mMockWindowManagerInternal.getFocusedWindowTokenFromWindowStates()) @@ -766,7 +783,8 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { public void onTouchInteractionEnd_noServiceInteractiveWindow_shouldClearA11yFocus() throws RemoteException { final IBinder defaultFocusWinToken = - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).token; + mWindows.get(Display.DEFAULT_DISPLAY).get( + DEFAULT_FOCUSED_INDEX).getWindowInfo().token; final int defaultFocusWindowId = mA11yWindowManager.findWindowIdLocked( USER_SYSTEM_ID, defaultFocusWinToken); when(mMockWindowManagerInternal.getFocusedWindowTokenFromWindowStates()) @@ -811,8 +829,8 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void getPictureInPictureWindow_shouldNotNull() { assertNull(mA11yWindowManager.getPictureInPictureWindowLocked()); - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(1).inPictureInPicture = true; - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + mWindows.get(Display.DEFAULT_DISPLAY).get(1).getWindowInfo().inPictureInPicture = true; + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); assertNotNull(mA11yWindowManager.getPictureInPictureWindowLocked()); } @@ -826,8 +844,9 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { final IAccessibilityInteractionConnection mockRemoteConnection = mA11yWindowManager.getConnectionLocked( USER_SYSTEM_ID, outsideWindowId).getRemote(); - mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0).hasFlagWatchOutsideTouch = true; - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + mWindows.get(Display.DEFAULT_DISPLAY).get(0).getWindowInfo().hasFlagWatchOutsideTouch = + true; + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); mA11yWindowManager.notifyOutsideTouch(USER_SYSTEM_ID, targetWindowId); verify(mockRemoteConnection).notifyOutsideTouch(); @@ -945,18 +964,14 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void sendAccessibilityEventOnWindowRemoval() { - final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY); + final ArrayList<AccessibilityWindow> windows = mWindows.get(Display.DEFAULT_DISPLAY); // Removing index 0 because it's not focused, and avoids unnecessary layer change. final int windowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); - infos.remove(0); - for (WindowInfo info : infos) { - // Adjust layer number because it should start from 0. - info.layer--; - } + windows.remove(0); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); final ArgumentCaptor<AccessibilityEvent> captor = ArgumentCaptor.forClass(AccessibilityEvent.class); @@ -970,21 +985,15 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void sendAccessibilityEventOnWindowAddition() throws RemoteException { - final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY); - - for (WindowInfo info : infos) { - // Adjust layer number because new window will have 0 so that layer number in - // A11yWindowInfo in window won't be changed. - info.layer++; - } + final ArrayList<AccessibilityWindow> windows = mWindows.get(Display.DEFAULT_DISPLAY); final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, false, USER_SYSTEM_ID); - addWindowInfo(infos, token, 0); - final int windowId = - getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, infos.size() - 1); + // Adding window to the front so that other windows' layer won't change. + windows.add(0, createMockAccessibilityWindow(token, Display.DEFAULT_DISPLAY)); + final int windowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); final ArgumentCaptor<AccessibilityEvent> captor = ArgumentCaptor.forClass(AccessibilityEvent.class); @@ -998,11 +1007,11 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { @Test public void sendAccessibilityEventOnWindowChange() { - final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY); - infos.get(0).title = "new title"; + final ArrayList<AccessibilityWindow> windows = mWindows.get(Display.DEFAULT_DISPLAY); + windows.get(0).getWindowInfo().title = "new title"; final int windowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); - onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); final ArgumentCaptor<AccessibilityEvent> captor = ArgumentCaptor.forClass(AccessibilityEvent.class); @@ -1020,42 +1029,41 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { } private void startTrackingPerDisplay(int displayId) throws RemoteException { - ArrayList<WindowInfo> windowInfosForDisplay = new ArrayList<>(); + ArrayList<AccessibilityWindow> windowsForDisplay = new ArrayList<>(); // Adds RemoteAccessibilityConnection into AccessibilityWindowManager, and copy // mock window token into mA11yWindowTokens. Also, preparing WindowInfo mWindowInfos // for the test. - int layer = 0; for (int i = 0; i < NUM_GLOBAL_WINDOWS; i++) { final IWindow token = addAccessibilityInteractionConnection(displayId, true, USER_SYSTEM_ID); - addWindowInfo(windowInfosForDisplay, token, layer++); + windowsForDisplay.add(createMockAccessibilityWindow(token, displayId)); } for (int i = 0; i < NUM_APP_WINDOWS; i++) { final IWindow token = addAccessibilityInteractionConnection(displayId, false, USER_SYSTEM_ID); - addWindowInfo(windowInfosForDisplay, token, layer++); + windowsForDisplay.add(createMockAccessibilityWindow(token, displayId)); } // Sets up current focused window of display. // Each display has its own current focused window if config_perDisplayFocusEnabled is true. // Otherwise only default display needs to current focused window. if (mSupportPerDisplayFocus || displayId == Display.DEFAULT_DISPLAY) { - windowInfosForDisplay.get(DEFAULT_FOCUSED_INDEX).focused = true; + windowsForDisplay.get(DEFAULT_FOCUSED_INDEX).getWindowInfo().focused = true; } // Turns on windows tracking, and update window info. mA11yWindowManager.startTrackingWindows(displayId, false); // Puts window lists into array. - mWindowInfos.put(displayId, windowInfosForDisplay); + mWindows.put(displayId, windowsForDisplay); // Sets the default display is the top focused display and // its current focused window is the top focused window. if (displayId == Display.DEFAULT_DISPLAY) { setTopFocusedWindowAndDisplay(displayId, DEFAULT_FOCUSED_INDEX); } // Invokes callback for sending window lists to A11y framework. - onWindowsForAccessibilityChanged(displayId, FORCE_SEND); + onAccessibilityWindowsChanged(displayId, FORCE_SEND); assertEquals(mA11yWindowManager.getWindowListLocked(displayId).size(), - windowInfosForDisplay.size()); + windowsForDisplay.size()); } private WindowsForAccessibilityCallback getWindowsForAccessibilityCallbacks(int displayId) { @@ -1109,36 +1117,28 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { return windowId; } - private void addWindowInfo(ArrayList<WindowInfo> windowInfos, IWindow windowToken, int layer) { - final WindowInfo windowInfo = WindowInfo.obtain(); - windowInfo.type = AccessibilityWindowInfo.TYPE_APPLICATION; - windowInfo.token = windowToken.asBinder(); - windowInfo.layer = layer; - windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); - windowInfos.add(windowInfo); - } - private int getWindowIdFromWindowInfosForDisplay(int displayId, int index) { - final IBinder windowToken = mWindowInfos.get(displayId).get(index).token; + final IBinder windowToken = mWindows.get(displayId).get(index).getWindowInfo().token; return mA11yWindowManager.findWindowIdLocked( USER_SYSTEM_ID, windowToken); } private void setTopFocusedWindowAndDisplay(int displayId, int index) { // Sets the top focus window. - mTopFocusedWindowToken = mWindowInfos.get(displayId).get(index).token; + mTopFocusedWindowToken = mWindows.get(displayId).get(index).getWindowInfo().token; // Sets the top focused display. mTopFocusedDisplayId = displayId; } - private void onWindowsForAccessibilityChanged(int displayId, boolean forceSend) { + private void onAccessibilityWindowsChanged(int displayId, boolean forceSend) { WindowsForAccessibilityCallback callbacks = mCallbackOfWindows.get(displayId); if (callbacks == null) { callbacks = getWindowsForAccessibilityCallbacks(displayId); mCallbackOfWindows.put(displayId, callbacks); } - callbacks.onWindowsForAccessibilityChanged(forceSend, mTopFocusedDisplayId, - mTopFocusedWindowToken, mWindowInfos.get(displayId)); + callbacks.onAccessibilityWindowsChanged(forceSend, mTopFocusedDisplayId, + mTopFocusedWindowToken, new Point(SCREEN_WIDTH, SCREEN_HEIGHT), + mWindows.get(displayId)); } private void changeFocusedWindowOnDisplayPerDisplayFocusConfig( @@ -1147,23 +1147,23 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { if (mSupportPerDisplayFocus) { // Gets the old focused window of display which wants to change focused window. WindowInfo focusedWindowInfo = - mWindowInfos.get(changeFocusedDisplayId).get(oldFocusedWindowIndex); + mWindows.get(changeFocusedDisplayId).get(oldFocusedWindowIndex).getWindowInfo(); // Resets the focus of old focused window. focusedWindowInfo.focused = false; // Gets the new window of display which wants to change focused window. focusedWindowInfo = - mWindowInfos.get(changeFocusedDisplayId).get(newFocusedWindowIndex); + mWindows.get(changeFocusedDisplayId).get(newFocusedWindowIndex).getWindowInfo(); // Sets the focus of new focused window. focusedWindowInfo.focused = true; } else { // Gets the window of display which wants to change focused window. WindowInfo focusedWindowInfo = - mWindowInfos.get(changeFocusedDisplayId).get(newFocusedWindowIndex); + mWindows.get(changeFocusedDisplayId).get(newFocusedWindowIndex).getWindowInfo(); // Sets the focus of new focused window. focusedWindowInfo.focused = true; // Gets the old focused window of old top focused display. focusedWindowInfo = - mWindowInfos.get(oldTopFocusedDisplayId).get(oldFocusedWindowIndex); + mWindows.get(oldTopFocusedDisplayId).get(oldFocusedWindowIndex).getWindowInfo(); // Resets the focus of old focused window. focusedWindowInfo.focused = false; // Changes the top focused display and window. @@ -1171,6 +1171,42 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { } } + private AccessibilityWindow createMockAccessibilityWindow(IWindow windowToken, int displayId) { + final WindowInfo windowInfo = WindowInfo.obtain(); + // TODO(b/325341171): add tests with various kinds of windows such as + // changing window types, touchable or not, trusted or not, etc. + windowInfo.type = WindowManager.LayoutParams.TYPE_APPLICATION; + windowInfo.token = windowToken.asBinder(); + + final AccessibilityWindow window = Mockito.mock(AccessibilityWindow.class); + when(window.getWindowInfo()).thenReturn(windowInfo); + when(window.ignoreRecentsAnimationForAccessibility()).thenReturn(false); + when(window.isFocused()).thenAnswer(invocation -> windowInfo.focused); + when(window.isTouchable()).thenReturn(true); + when(window.getType()).thenReturn(windowInfo.type); + + setRegionForMockAccessibilityWindow(window, nextToucableRegion(displayId)); + return window; + } + + private void setRegionForMockAccessibilityWindow(AccessibilityWindow window, Region region) { + doAnswer(invocation -> { + ((Region) invocation.getArgument(0)).set(region); + return null; + }).when(window).getTouchableRegionInScreen(any(Region.class)); + doAnswer(invocation -> { + ((Region) invocation.getArgument(0)).set(region); + return null; + }).when(window).getTouchableRegionInWindow(any(Region.class)); + } + + private Region nextToucableRegion(int displayId) { + final int topLeft = mNextRegionOffsets.get(displayId, 0); + final int bottomRight = topLeft + 100; + mNextRegionOffsets.put(displayId, topLeft + 10); + return new Region(topLeft, topLeft, bottomRight, bottomRight); + } + @Nullable private static String toString(@Nullable CharSequence cs) { return cs == null ? null : cs.toString(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 8c0d44c46814..7fbd521b5432 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -189,6 +189,8 @@ public class FullScreenMagnificationGestureHandlerTest { FullScreenMagnificationVibrationHelper mMockFullScreenMagnificationVibrationHelper; @Mock FullScreenMagnificationGestureHandler.MagnificationLogger mMockMagnificationLogger; + @Mock + OneFingerPanningSettingsProvider mMockOneFingerPanningSettingsProvider; @Rule public final TestableContext mContext = new TestableContext(getInstrumentation().getContext()); @@ -266,6 +268,7 @@ public class FullScreenMagnificationGestureHandlerTest { mMgh.onDestroy(); mFullScreenMagnificationController.unregister(DISPLAY_0); verify(mWindowMagnificationPromptController).onDestroy(); + verify(mMockOneFingerPanningSettingsProvider).unregister(); Settings.Secure.putFloatForUser(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, mOriginalMagnificationPersistedScale, @@ -288,11 +291,10 @@ public class FullScreenMagnificationGestureHandlerTest { DISPLAY_0, mMockFullScreenMagnificationVibrationHelper, mMockMagnificationLogger, - ViewConfiguration.get(mContext)); + ViewConfiguration.get(mContext), + mMockOneFingerPanningSettingsProvider); if (isWatch()) { - h.setSinglePanningEnabled(true); - } else { - h.setSinglePanningEnabled(false); + enableOneFingerPanning(true); } mHandler = new TestHandler(h.mDetectingState, mClock) { @Override @@ -607,8 +609,8 @@ public class FullScreenMagnificationGestureHandlerTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) public void testTwoFingerTap_StateIsActivated_shouldInDelegating() { - assumeTrue(mMgh.mIsSinglePanningEnabled); - mMgh.setSinglePanningEnabled(false); + assumeTrue(isWatch()); + enableOneFingerPanning(false); goFromStateIdleTo(STATE_ACTIVATED); allowEventDelegation(); @@ -623,8 +625,8 @@ public class FullScreenMagnificationGestureHandlerTest { @Test @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) public void testTwoFingerTap_StateIsIdle_shouldInDelegating() { - assumeTrue(mMgh.mIsSinglePanningEnabled); - mMgh.setSinglePanningEnabled(false); + assumeTrue(isWatch()); + enableOneFingerPanning(false); goFromStateIdleTo(STATE_IDLE); allowEventDelegation(); @@ -830,7 +832,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test public void testActionUpNotAtEdge_singlePanningState_detectingState() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); send(upEvent()); @@ -841,8 +843,8 @@ public class FullScreenMagnificationGestureHandlerTest { @Test public void testScroll_SinglePanningDisabled_delegatingState() { - assumeTrue(mMgh.mIsSinglePanningEnabled); - mMgh.setSinglePanningEnabled(false); + assumeTrue(isWatch()); + enableOneFingerPanning(false); goFromStateIdleTo(STATE_ACTIVATED); allowEventDelegation(); @@ -854,7 +856,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test @FlakyTest public void testScroll_singleHorizontalPanningAndAtEdge_leftEdgeOverscroll() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); float centerY = (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f; @@ -878,7 +880,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test @FlakyTest public void testScroll_singleHorizontalPanningAndAtEdge_rightEdgeOverscroll() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); float centerY = (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f; @@ -902,7 +904,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test @FlakyTest public void testScroll_singleVerticalPanningAndAtEdge_verticalOverscroll() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); float centerX = (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f; @@ -924,7 +926,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test public void testScroll_singlePanningAndAtEdge_noOverscroll() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); float centerY = (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f; @@ -946,7 +948,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test public void testScroll_singleHorizontalPanningAndAtEdge_vibrate() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); mFullScreenMagnificationController.setCenter( DISPLAY_0, @@ -970,7 +972,7 @@ public class FullScreenMagnificationGestureHandlerTest { @Test public void testScroll_singleVerticalPanningAndAtEdge_doNotVibrate() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_SINGLE_PANNING); mFullScreenMagnificationController.setCenter( DISPLAY_0, @@ -993,8 +995,9 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE) public void singleFinger_testScrollAfterMagnified_startsFling() { - assumeTrue(mMgh.mIsSinglePanningEnabled); + assumeTrue(isWatch()); goFromStateIdleTo(STATE_ACTIVATED); swipeAndHold(); @@ -1274,6 +1277,10 @@ public class FullScreenMagnificationGestureHandlerTest { mFullScreenMagnificationController.reset(DISPLAY_0, /* animate= */ false); } + private void enableOneFingerPanning(boolean enable) { + when(mMockOneFingerPanningSettingsProvider.isOneFingerPanningEnabled()).thenReturn(enable); + } + private void assertActionsInOrder(List<MotionEvent> actualEvents, List<Integer> expectedActions) { assertTrue(actualEvents.size() == expectedActions.size()); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OneFingerPanningSettingsProviderTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OneFingerPanningSettingsProviderTest.java new file mode 100644 index 000000000000..ac46ef9afa40 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OneFingerPanningSettingsProviderTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2020 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.accessibility.magnification; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.provider.Settings; +import android.testing.TestableContext; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.accessibility.magnification.OneFingerPanningSettingsProvider.State; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class OneFingerPanningSettingsProviderTest { + + @Rule + public final TestableContext mContext = new TestableContext(getInstrumentation().getContext()); + + private boolean mDefaultValue; + private boolean mOriginalIsOneFingerPanningEnabled; + + private OneFingerPanningSettingsProvider mProvider; + + @Before + public void setup() { + mDefaultValue = OneFingerPanningSettingsProvider.isOneFingerPanningEnabledDefault(mContext); + mOriginalIsOneFingerPanningEnabled = isSecureSettingsEnabled(); + } + + @After + public void tearDown() { + enableSecureSettings(mOriginalIsOneFingerPanningEnabled); + if (mProvider != null) { + mProvider.unregister(); + } + } + + @Test + public void isOneFingerPanningEnabled_flagDisabled_matchesDefault() { + mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ false); + + assertThat(mProvider.isOneFingerPanningEnabled()).isEqualTo(mDefaultValue); + } + + @Test + public void isOneFingerPanningEnabled_flagEnabledSettingEnabled_true() { + enableSecureSettings(true); + mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true); + + assertTrue(mProvider.isOneFingerPanningEnabled()); + } + + @Test + public void isOneFingerPanningEnabled_flagEnabledSettingDisabled_false() { + enableSecureSettings(false); + mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true); + + assertFalse(mProvider.isOneFingerPanningEnabled()); + } + + @Test + public void isOneFingerPanningEnabled_flagEnabledSettingsFalse_false() { + mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true); + + // Simulate observer triggered. + enableSecureSettings(false); + mProvider.mObserver.onChange(/* selfChange= */ false); + + assertFalse(mProvider.isOneFingerPanningEnabled()); + } + + @Test + public void isOneFingerPanningEnabled_flagEnabledSettingsTrue_true() { + mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ true); + + // Simulate observer triggered. + enableSecureSettings(true); + mProvider.mObserver.onChange(/* selfChange= */ false); + + assertTrue(mProvider.isOneFingerPanningEnabled()); + } + + @Test + public void isOneFingerPanningEnabled_flagDisabledSettingsChanges_valueUnchanged() { + mProvider = new OneFingerPanningSettingsProvider(mContext, /* featureFlagEnabled */ false); + var previousValue = mProvider.isOneFingerPanningEnabled(); + + enableSecureSettings(!previousValue); + + assertThat(mProvider.isOneFingerPanningEnabled()).isEqualTo(previousValue); + assertThat(mProvider.isOneFingerPanningEnabled()).isEqualTo(mDefaultValue); + } + + @Test + public void unregister_featureEnabled_contentResolverNull() { + var provider = new OneFingerPanningSettingsProvider( + mContext, /* featureFlagEnabled */ true); + + provider.unregister(); + + assertThat(provider.mContentResolver).isNull(); + } + + @Test + public void unregister_featureDisabled_noError() { + var provider = new OneFingerPanningSettingsProvider( + mContext, /* featureFlagEnabled */ false); + + provider.unregister(); + } + + private void enableSecureSettings(boolean enable) { + Settings.Secure.putIntForUser( + mContext.getContentResolver(), + OneFingerPanningSettingsProvider.KEY, + enable ? State.ON : State.OFF, + mContext.getUserId()); + } + + private boolean isSecureSettingsEnabled() { + return State.ON == Settings.Secure.getIntForUser( + mContext.getContentResolver(), + OneFingerPanningSettingsProvider.KEY, + mDefaultValue ? State.ON : State.OFF, + mContext.getUserId()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java index bb0063427339..fa1fd90e10c9 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java @@ -344,6 +344,21 @@ public class ALSProbeTest { verifyNoMoreInteractions(mSensorManager); } + @Test + public void testAwaitLuxWhenNoLightSensor() { + when(mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(null); + mProbe = new ALSProbe(mSensorManager, new Handler(mLooper.getLooper()), TIMEOUT_MS - 1); + + AtomicInteger lux = new AtomicInteger(-5); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + + // Verify that no light sensor will be registered. + verify(mSensorManager, times(0)).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + + assertThat(lux.get()).isEqualTo(-1); + } + private void moveTimeBy(long millis) { mLooper.moveTimeForward(millis); mLooper.processAllMessages(); diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index e0ef035de735..a6f2196cf05b 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -30,6 +30,7 @@ import android.app.admin.flags.Flags; import android.app.role.RoleManager; import android.content.Context; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.BugreportManager.BugreportCallback; import android.os.BugreportParams; @@ -37,6 +38,7 @@ import android.os.IBinder; import android.os.IDumpstateListener; import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -67,6 +69,9 @@ import java.util.function.Consumer; @RunWith(AndroidJUnit4.class) public class BugreportManagerServiceImplTest { + private static final UserInfo ADMIN_USER_INFO = + new UserInfo(/* id= */ 5678, "adminUser", UserInfo.FLAG_ADMIN); + @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -82,6 +87,8 @@ public class BugreportManagerServiceImplTest { @Mock private DevicePolicyManager mMockDevicePolicyManager; + private TestInjector mInjector; + private int mCallingUid = 1234; private String mCallingPackage = "test.package"; private AtomicFile mMappingFile; @@ -96,9 +103,9 @@ public class BugreportManagerServiceImplTest { mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml"); ArraySet<String> mAllowlistedPackages = new ArraySet<>(); mAllowlistedPackages.add(mContext.getPackageName()); - mService = new BugreportManagerServiceImpl( - new TestInjector(mContext, mAllowlistedPackages, mMappingFile, - mMockUserManager, mMockDevicePolicyManager)); + mInjector = new TestInjector(mContext, mAllowlistedPackages, mMappingFile, + mMockUserManager, mMockDevicePolicyManager); + mService = new BugreportManagerServiceImpl(mInjector); mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile); when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid); // The calling user is an admin user by default. @@ -190,6 +197,33 @@ public class BugreportManagerServiceImplTest { } @Test + public void testStartBugreport() throws Exception { + mService.startBugreport(mCallingUid, mContext.getPackageName(), + new FileDescriptor(), /* screenshotFd= */ null, + BugreportParams.BUGREPORT_MODE_FULL, + /* flags= */ 0, new Listener(new CountDownLatch(1)), + /* isScreenshotRequested= */ false); + + assertThat(mInjector.isBugreportStarted()).isTrue(); + } + + @Test + public void testStartBugreport_nonAdminProfileOfAdminCurrentUser() throws Exception { + int callingUid = Binder.getCallingUid(); + int callingUserId = UserHandle.getUserId(callingUid); + when(mMockUserManager.isUserAdmin(callingUserId)).thenReturn(false); + when(mMockUserManager.getProfileParent(callingUserId)).thenReturn(ADMIN_USER_INFO); + + mService.startBugreport(mCallingUid, mContext.getPackageName(), + new FileDescriptor(), /* screenshotFd= */ null, + BugreportParams.BUGREPORT_MODE_FULL, + /* flags= */ 0, new Listener(new CountDownLatch(1)), + /* isScreenshotRequested= */ false); + + assertThat(mInjector.isBugreportStarted()).isTrue(); + } + + @Test public void testStartBugreport_throwsForNonAdminUser() throws Exception { when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false); @@ -317,8 +351,12 @@ public class BugreportManagerServiceImplTest { private static class TestInjector extends BugreportManagerServiceImpl.Injector { + private static final String SYSTEM_PROPERTY_BUGREPORT_START = "ctl.start"; + private static final String SYSTEM_PROPERTY_BUGREPORT_STOP = "ctl.stop"; + private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; + private boolean mBugreportStarted = false; TestInjector(Context context, ArraySet<String> allowlistedPackages, AtomicFile mappingFile, UserManager um, DevicePolicyManager dpm) { @@ -336,5 +374,20 @@ public class BugreportManagerServiceImplTest { public DevicePolicyManager getDevicePolicyManager() { return mDevicePolicyManager; } + + @Override + public void setSystemProperty(String key, String value) { + // Calling SystemProperties.set() will throw a RuntimeException due to permission error. + // Instead, we are just marking a flag to store the state for testing. + if (SYSTEM_PROPERTY_BUGREPORT_START.equals(key)) { + mBugreportStarted = true; + } else if (SYSTEM_PROPERTY_BUGREPORT_STOP.equals(key)) { + mBugreportStarted = false; + } + } + + public boolean isBugreportStarted() { + return mBugreportStarted; + } } } diff --git a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java index b9ece9360980..e27bb4c8c3b6 100644 --- a/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java +++ b/services/tests/servicestests/utils/com/android/server/testutils/StubTransaction.java @@ -40,19 +40,12 @@ import java.util.concurrent.Executor; public class StubTransaction extends SurfaceControl.Transaction { private HashSet<Runnable> mWindowInfosReportedListeners = new HashSet<>(); - private HashSet<SurfaceControl.TransactionCommittedListener> mTransactionCommittedListeners = - new HashSet<>(); @Override public void apply() { for (Runnable listener : mWindowInfosReportedListeners) { listener.run(); } - for (SurfaceControl.TransactionCommittedListener listener - : mTransactionCommittedListeners) { - listener.onTransactionCommitted(); - } - mTransactionCommittedListeners.clear(); } @Override @@ -246,9 +239,6 @@ public class StubTransaction extends SurfaceControl.Transaction { @Override public SurfaceControl.Transaction addTransactionCommittedListener(Executor executor, SurfaceControl.TransactionCommittedListener listener) { - SurfaceControl.TransactionCommittedListener listenerInner = - () -> executor.execute(listener::onTransactionCommitted); - mTransactionCommittedListeners.add(listenerInner); return this; } diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 2f29d10ec2f9..515898a883e8 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -48,6 +48,8 @@ android_test { "notification_flags_lib", "platform-test-rules", "SettingsLib", + "libprotobuf-java-lite", + "platformprotoslite", ], libs: [ diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 4dded1d0342d..05b6c907069b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -37,6 +37,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -885,6 +886,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); + mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); service.addApprovedList("a", 0, true); service.reregisterService(cn, 0); @@ -915,6 +917,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); + mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); service.addApprovedList("a", 0, false); service.reregisterService(cn, 0); @@ -945,6 +948,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); + mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); service.addApprovedList("a/a", 0, true); service.reregisterService(cn, 0); @@ -975,6 +979,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); + mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); service.addApprovedList("a/a", 0, false); service.reregisterService(cn, 0); @@ -1152,6 +1157,58 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + public void testUpgradeAppNoPermissionNoRebind() throws Exception { + Context context = spy(getContext()); + doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, + mIpm, + APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1"); + final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2"); + + // Both components are approved initially + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + //Component package/C1 loses bind permission + when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer( + (Answer<ServiceInfo>) invocation -> { + ComponentName invocationCn = invocation.getArgument(0); + if (invocationCn != null) { + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationCn.getPackageName(); + serviceInfo.name = invocationCn.getClassName(); + if (invocationCn.equals(unapprovedComponent)) { + serviceInfo.permission = "none"; + } else { + serviceInfo.permission = service.getConfig().bindPermission; + } + serviceInfo.metaData = null; + return serviceInfo; + } + return null; + } + ); + + // Trigger package update + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent)); + assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent)); + } + + @Test public void testSetPackageOrComponentEnabled() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java index 33ca5c2bbe16..a45b102278ef 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java @@ -20,6 +20,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.INotificationManager; import android.content.ComponentName; import android.content.Context; @@ -47,9 +49,12 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; import android.util.Xml; +import android.Manifest; +import com.android.internal.util.CollectionUtils; import com.android.internal.util.function.TriPredicate; import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; import com.android.server.UiServiceTestCase; import com.android.server.notification.NotificationManagerService.NotificationAssistants; @@ -59,7 +64,9 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -89,11 +96,15 @@ public class NotificationAssistantsTest extends UiServiceTestCase { UserInfo mZero = new UserInfo(0, "zero", 0); UserInfo mTen = new UserInfo(10, "ten", 0); + ComponentName mCn = new ComponentName("a", "b"); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mContext.setMockPackageManager(mPm); mContext.addMockSystemService(Context.USER_SERVICE, mUm); + mContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.string.config_defaultAssistantAccessComponent, "a/a"); mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm)); when(mNm.getBinderService()).thenReturn(mINm); mContext.ensureTestableResources(); @@ -102,8 +113,9 @@ public class NotificationAssistantsTest extends UiServiceTestCase { ResolveInfo resolve = new ResolveInfo(); approved.add(resolve); ServiceInfo info = new ServiceInfo(); - info.packageName = "a"; - info.name="a"; + info.packageName = mCn.getPackageName(); + info.name = mCn.getClassName(); + info.permission = Manifest.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE; resolve.serviceInfo = info; when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())) .thenReturn(approved); @@ -137,6 +149,51 @@ public class NotificationAssistantsTest extends UiServiceTestCase { } @Test + public void testWriteXml_userTurnedOffNAS() throws Exception { + int userId = ActivityManager.getCurrentUser(); + + mAssistants.loadDefaultsFromConfig(true); + + mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true, + true, true); + + ComponentName current = CollectionUtils.firstOrNull( + mAssistants.getAllowedComponents(userId)); + assertNotNull(current); + mAssistants.setUserSet(userId, true); + mAssistants.setPackageOrComponentEnabled(current.flattenToString(), userId, true, false, + true); + + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + mAssistants.writeXml(serializer, true, userId); + serializer.endDocument(); + serializer.flush(); + + //fail(baos.toString("UTF-8")); + + final TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + TriPredicate<String, Integer, String> allowedManagedServicePackages = + mNm::canUseManagedServices; + + parser.nextTag(); + mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm)); + mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL); + + ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0); + // approved should not be null + assertNotNull(approved); + assertEquals(new ArraySet<>(), approved.get(true)); + + // user set is maintained + assertTrue(mAssistants.mIsUserChanged.get(ActivityManager.getCurrentUser())); + } + + @Test public void testReadXml_userDisabled() throws Exception { String xml = "<enabled_assistants version=\"4\" defaults=\"b/b\">" + "<service_listing approved=\"\" user=\"0\" primary=\"true\"" @@ -160,6 +217,33 @@ public class NotificationAssistantsTest extends UiServiceTestCase { } @Test + public void testReadXml_userDisabled_restore() throws Exception { + String xml = "<enabled_assistants version=\"4\" defaults=\"b/b\">" + + "<service_listing approved=\"\" user=\"0\" primary=\"true\"" + + "user_changed=\"true\"/>" + + "</enabled_assistants>"; + + final TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(xml.toString().getBytes())), null); + TriPredicate<String, Integer, String> allowedManagedServicePackages = + mNm::canUseManagedServices; + + parser.nextTag(); + mAssistants.readXml(parser, allowedManagedServicePackages, true, + ActivityManager.getCurrentUser()); + + ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0); + + // approved should not be null + assertNotNull(approved); + assertEquals(new ArraySet<>(), approved.get(true)); + + // user set is maintained + assertTrue(mAssistants.mIsUserChanged.get(ActivityManager.getCurrentUser())); + } + + @Test public void testReadXml_upgradeUserSet() throws Exception { String xml = "<enabled_assistants version=\"3\" defaults=\"b/b\">" + "<service_listing approved=\"\" user=\"0\" primary=\"true\"" diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java index 3499a12f5954..bf8cfa5c0561 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryJobServiceTest.java @@ -20,8 +20,12 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.assertFalse; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -70,10 +74,11 @@ public class NotificationHistoryJobServiceTest extends UiServiceTestCase { @Before public void setUp() throws Exception { - mJobService = new NotificationHistoryJobService(); + mJobService = spy(new NotificationHistoryJobService()); mJobService.attachBaseContext(mContext); mJobService.onCreate(); mJobService.onBind(/* intent= */ null); // Create JobServiceEngine within JobService. + doNothing().when(mJobService).jobFinished(any(), eq(false)); mContext.addMockSystemService(JobScheduler.class, mMockJobScheduler); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 78fb5705d5ac..bfc47fdef5cb 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -324,7 +324,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mPermissionHelper.getNotificationPermissionValues(USER_SYSTEM)) .thenReturn(appPermissions); - when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0})); + IntArray currentProfileIds = IntArray.wrap(new int[]{0}); + if (UserManager.isHeadlessSystemUserMode()) { + currentProfileIds.add(UserHandle.getUserId(UID_HEADLESS)); + } + when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds); mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java index 99d5a6d9118a..75552bc433c5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java @@ -16,7 +16,7 @@ package com.android.server.notification; -import static com.android.server.notification.ZenAdapters.notificationPolicyToZenPolicy; +import static android.service.notification.ZenAdapters.notificationPolicyToZenPolicy; import static com.google.common.truth.Truth.assertThat; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java new file mode 100644 index 000000000000..f724510eeb73 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.AutomaticZenRule; +import android.provider.Settings; +import android.service.notification.ZenPolicy; +import android.test.suitebuilder.annotation.SmallTest; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.os.dnd.ActiveRuleType; +import com.android.os.dnd.ChannelPolicy; +import com.android.os.dnd.ConversationType; +import com.android.os.dnd.PeopleType; +import com.android.os.dnd.State; +import com.android.os.dnd.ZenMode; + +import com.google.protobuf.Internal; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** Test to validate that logging enums used in Zen classes match their API definitions. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ZenEnumTest { + + @Test + public void testEnum_zenMode() { + testEnum(Settings.Global.class, "ZEN_MODE", ZenMode.class, "ZEN_MODE"); + } + + @Test + public void testEnum_activeRuleType() { + testEnum(AutomaticZenRule.class, "TYPE", ActiveRuleType.class, "TYPE"); + } + + @Test + public void testEnum_zenPolicyState() { + testEnum(ZenPolicy.class, "STATE", State.class, "STATE"); + } + + @Test + public void testEnum_zenPolicyChannelPolicy() { + testEnum(ZenPolicy.class, "CHANNEL_POLICY", ChannelPolicy.class, "CHANNEL_POLICY"); + } + + @Test + public void testEnum_zenPolicyConversationType() { + testEnum(ZenPolicy.class, "CONVERSATION_SENDERS", ConversationType.class, "CONV"); + } + + @Test + public void testEnum_zenPolicyPeopleType() { + testEnum(ZenPolicy.class, "PEOPLE_TYPE", PeopleType.class, "PEOPLE"); + } + + /** + * Verifies that any constants (i.e. {@code public static final int} fields) named {@code + * <apiPrefix>_SOMETHING} in {@code apiClass} are present and have the same numerical value + * in the enum values defined in {@code loggingProtoEnumClass}. + * + * <p>Note that <em>extra</em> values in the logging enum are accepted (since we have one of + * those, and the main goal of this test is that we don't forget to update the logging enum + * if new API enum values are added). + */ + private static void testEnum(Class<?> apiClass, String apiPrefix, + Class<? extends Internal.EnumLite> loggingProtoEnumClass, + String loggingPrefix) { + Map<String, Integer> apiConstants = + Arrays.stream(apiClass.getDeclaredFields()) + .filter(f -> Modifier.isPublic(f.getModifiers())) + .filter(f -> Modifier.isStatic(f.getModifiers())) + .filter(f -> Modifier.isFinal(f.getModifiers())) + .filter(f -> f.getType().equals(int.class)) + .filter(f -> f.getName().startsWith(apiPrefix + "_")) + .collect(Collectors.toMap( + Field::getName, + ZenEnumTest::getStaticFieldIntValue)); + + Map<String, Integer> loggingConstants = + Arrays.stream(loggingProtoEnumClass.getEnumConstants()) + .collect(Collectors.toMap( + v -> v.toString(), + v -> v.getNumber())); + + Map<String, Integer> renamedApiConstants = apiConstants.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().replace(apiPrefix + "_", loggingPrefix + "_"), + Map.Entry::getValue)); + + assertThat(loggingConstants).containsAtLeastEntriesIn(renamedApiConstants); + } + + private static int getStaticFieldIntValue(Field f) { + try { + return f.getInt(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} 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 495e01a640de..7c1adbc39033 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -136,6 +136,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.service.notification.Condition; import android.service.notification.DeviceEffectsApplier; +import android.service.notification.ZenAdapters; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ConfigChangeOrigin; diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 1a1fe95756d7..0c1fbf3cb3d7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -94,7 +94,6 @@ public class AppTransitionControllerTest extends WindowTestsBase { public void setUp() throws Exception { assumeFalse(WindowManagerService.sEnableShellTransitions); mAppTransitionController = new AppTransitionController(mWm, mDisplayContent); - mWm.mAnimator.ready(); } @Test @@ -856,7 +855,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation run by the remote handler. assertTrue(remoteAnimationRunner.isAnimationStarted()); @@ -887,7 +886,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(openingActivity, closingActivity, null /* changingTaskFragment */); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation is not run by the remote handler because the activity is filling the Task. assertFalse(remoteAnimationRunner.isAnimationStarted()); @@ -922,7 +921,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(openingActivity, closingActivity, null /* changingTaskFragment */); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation run by the remote handler. assertTrue(remoteAnimationRunner.isAnimationStarted()); @@ -947,7 +946,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation run by the remote handler. assertTrue(remoteAnimationRunner.isAnimationStarted()); @@ -974,7 +973,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment1); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation run by the remote handler. assertTrue(remoteAnimationRunner.isAnimationStarted()); @@ -998,7 +997,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation not run by the remote handler. assertFalse(remoteAnimationRunner.isAnimationStarted()); @@ -1025,7 +1024,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(openingActivity, closingActivity, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation should not run by the remote handler when there are non-embedded activities of // different UID. @@ -1052,7 +1051,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // Animation should not run by the remote handler when there is wallpaper in the transition. assertFalse(remoteAnimationRunner.isAnimationStarted()); @@ -1086,7 +1085,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(activity1, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // The animation will be animated remotely by client and all activities are input disabled // for untrusted animation. @@ -1137,7 +1136,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // The animation will be animated remotely by client and all activities are input disabled // for untrusted animation. @@ -1179,7 +1178,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { // Prepare and start transition. prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); // The animation will be animated remotely by client, but input should not be dropped for // fully trusted. diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 1f15ec3be3a8..2085d6140f68 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -371,7 +371,6 @@ public class InsetsStateControllerTest extends WindowTestsBase { mDisplayContent.getInsetsPolicy().updateBarControlTarget(app); mDisplayContent.getInsetsPolicy().showTransient(statusBars(), true /* isGestureOnSystemBar */); - mWm.mAnimator.ready(); waitUntilWindowAnimatorIdle(); assertTrue(mDisplayContent.getInsetsPolicy().isTransient(statusBars())); diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index a1638019359b..11d9629cf25e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -43,6 +43,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -112,7 +113,6 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { runWithScissors(mWm.mH, () -> mHandler = new TestHandler(null, mClock), 0); mController = new RemoteAnimationController(mWm, mDisplayContent, mAdapter, mHandler, false /*isActivityEmbedding*/); - mWm.mAnimator.ready(); } private WindowState createAppOverlayWindow() { @@ -136,7 +136,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -168,7 +168,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -290,7 +290,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -336,7 +336,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { task.applyAnimationUnchecked(null /* lp */, true /* enter */, TRANSIT_OLD_TASK_OPEN, false /* isVoiceInteraction */, null /* sources */); mController.goodToGo(TRANSIT_OLD_TASK_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); try { @@ -363,7 +363,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback); mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -417,7 +417,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback); mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -471,7 +471,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback); mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -526,7 +526,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -559,7 +559,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -595,7 +595,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -645,7 +645,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = ArgumentCaptor.forClass(RemoteAnimationTarget[].class); final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = @@ -782,7 +782,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mDisplayContent.applySurfaceChangesTransaction(); mController.goodToGo(TRANSIT_OLD_TASK_OPEN); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_OPEN), any(), any(), any(), any()); @@ -810,7 +810,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, mFinishedCallback); mController.goodToGo(transit); - waitUntilWindowAnimatorIdle(); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); return adapter; } diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index 7ab093d0ae13..a8f6fe86c823 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -546,7 +546,7 @@ public class SystemServicesTestRule implements TestRule { // This makes sure all previous messages in the handler are fully processed vs. just popping // them from the message queue. final AtomicBoolean currentMessagesProcessed = new AtomicBoolean(false); - wm.mAnimator.addAfterPrepareSurfacesRunnable(() -> { + wm.mAnimator.getChoreographer().postFrameCallback(time -> { synchronized (currentMessagesProcessed) { currentMessagesProcessed.set(true); currentMessagesProcessed.notifyAll(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 01bd96b8a772..5360a1033eb4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -896,6 +896,11 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop)); // The focus should NOT change. assertEquals(winRightTop, mDisplayContent.mCurrentFocus); + + // Do not move focus if the dim is boosted. + taskFragmentLeft.mDimmerSurfaceBoosted = true; + assertFalse(mWm.moveFocusToTopEmbeddedWindow(winLeftTop)); + assertEquals(winRightTop, mDisplayContent.mCurrentFocus); } } diff --git a/telecomm/java/android/telecom/RemoteConnectionService.java b/telecomm/java/android/telecom/RemoteConnectionService.java index 2fc6a22261b6..8b503f2b97e4 100644 --- a/telecomm/java/android/telecom/RemoteConnectionService.java +++ b/telecomm/java/android/telecom/RemoteConnectionService.java @@ -28,6 +28,8 @@ import com.android.internal.telecom.IConnectionService; import com.android.internal.telecom.IConnectionServiceAdapter; import com.android.internal.telecom.IVideoProvider; import com.android.internal.telecom.RemoteServiceCallback; +import com.android.server.telecom.flags.FeatureFlags; +import com.android.server.telecom.flags.FeatureFlagsImpl; import java.util.ArrayList; import java.util.HashMap; @@ -550,6 +552,9 @@ final class RemoteConnectionService { private final Map<String, RemoteConference> mConferenceById = new HashMap<>(); private final Set<RemoteConnection> mPendingConnections = new HashSet<>(); + /** Telecom feature flags **/ + private final FeatureFlags mTelecomFeatureFlags = new FeatureFlagsImpl(); + RemoteConnectionService( IConnectionService outgoingConnectionServiceRpc, ConnectionService ourConnectionServiceImpl) throws RemoteException { @@ -578,6 +583,14 @@ final class RemoteConnectionService { extras.putString(Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME, mOurConnectionServiceImpl.getApplicationContext().getOpPackageName()); + // Defaulted ConnectionRequest params + String telecomCallId = ""; + boolean shouldShowIncomingUI = false; + if (mTelecomFeatureFlags.setRemoteConnectionCallId()) { + telecomCallId = id; + shouldShowIncomingUI = request.shouldShowIncomingCallUi(); + } + final ConnectionRequest newRequest = new ConnectionRequest.Builder() .setAccountHandle(request.getAccountHandle()) .setAddress(request.getAddress()) @@ -585,6 +598,9 @@ final class RemoteConnectionService { .setVideoState(request.getVideoState()) .setRttPipeFromInCall(request.getRttPipeFromInCall()) .setRttPipeToInCall(request.getRttPipeToInCall()) + // Flagged changes + .setTelecomCallId(telecomCallId) + .setShouldShowIncomingCallUi(shouldShowIncomingUI) .build(); try { if (mConnectionById.isEmpty()) { @@ -626,10 +642,28 @@ final class RemoteConnectionService { mOutgoingConnectionServiceRpc.addConnectionServiceAdapter(mServant.getStub(), null /*Session.Info*/); } + + // Set telecom call id to what's being tracked by base ConnectionService. + String telecomCallId = mTelecomFeatureFlags.setRemoteConnectionCallId() + ? id : request.getTelecomCallId(); + + final ConnectionRequest newRequest = new ConnectionRequest.Builder() + .setAccountHandle(request.getAccountHandle()) + .setAddress(request.getAddress()) + .setExtras(request.getExtras()) + .setVideoState(request.getVideoState()) + .setShouldShowIncomingCallUi(request.shouldShowIncomingCallUi()) + .setRttPipeFromInCall(request.getRttPipeFromInCall()) + .setRttPipeToInCall(request.getRttPipeToInCall()) + .setParticipants(request.getParticipants()) + .setIsAdhocConferenceCall(request.isAdhocConferenceCall()) + .setTelecomCallId(telecomCallId) + .build(); + RemoteConference conference = new RemoteConference(id, mOutgoingConnectionServiceRpc); mOutgoingConnectionServiceRpc.createConference(connectionManagerPhoneAccount, id, - request, + newRequest, isIncoming, false /* isUnknownCall */, null /*Session.info*/); @@ -640,7 +674,7 @@ final class RemoteConnectionService { maybeDisconnectAdapter(); } }); - conference.putExtras(request.getExtras()); + conference.putExtras(newRequest.getExtras()); return conference; } catch (RemoteException e) { return RemoteConference.failure( diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 5d99acd87dd3..2150b5deff52 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -5307,6 +5307,19 @@ public class CarrierConfigManager { KEY_PREFIX + "enable_presence_group_subscribe_bool"; /** + * SIP SUBSCRIBE retry duration used when device doesn't receive a response to SIP + * SUBSCRIBE request. + * If this value is not defined or defined as negative value, the device does not retry + * the SIP SUBSCRIBE. + * If the value is 0 then device retries immediately upon timeout. + * If the value is > 0 then device waits for configured duration and retries after timeout + * is detected + * @hide + */ + public static final String KEY_SUBSCRIBE_RETRY_DURATION_MILLIS_LONG = + KEY_PREFIX + "subscribe_retry_duration_millis_long"; + + /** * Flag indicating whether or not to use SIP URI when send a presence subscribe. * When {@code true}, the device sets the To and Contact header to be SIP URI using * the TelephonyManager#getIsimDomain" API. @@ -5982,6 +5995,7 @@ public class CarrierConfigManager { defaults.putBoolean(KEY_ENABLE_PRESENCE_CAPABILITY_EXCHANGE_BOOL, false); defaults.putBoolean(KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, false); defaults.putBoolean(KEY_ENABLE_PRESENCE_GROUP_SUBSCRIBE_BOOL, false); + defaults.putInt(KEY_SUBSCRIBE_RETRY_DURATION_MILLIS_LONG, -1); defaults.putBoolean(KEY_USE_SIP_URI_FOR_PRESENCE_SUBSCRIBE_BOOL, false); defaults.putInt(KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT, 30 * 24 * 60 * 60); defaults.putBoolean(KEY_RCS_REQUEST_FORBIDDEN_BY_SIP_489_BOOL, false); diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp index 1d71f95ef64f..d658d5991a57 100644 --- a/tests/FlickerTests/Android.bp +++ b/tests/FlickerTests/Android.bp @@ -63,17 +63,20 @@ java_library { ], } -android_library_import { - name: "wm-flicker-window-extensions_nodeps", - aars: ["libs/window-extensions-release.aar"], +java_library { + name: "wm-flicker-window-extensions", sdk_version: "current", + static_libs: [ + "androidx.window.extensions_extensions-nodeps", + ], + installable: false, } java_library { - name: "wm-flicker-window-extensions", + name: "wm-flicker-window-extensions-core", sdk_version: "current", static_libs: [ - "wm-flicker-window-extensions_nodeps", + "androidx.window.extensions.core_core-nodeps", ], installable: false, } diff --git a/tests/FlickerTests/libs/window-extensions-release.aar b/tests/FlickerTests/libs/window-extensions-release.aar Binary files differdeleted file mode 100644 index 918e514f4c89..000000000000 --- a/tests/FlickerTests/libs/window-extensions-release.aar +++ /dev/null diff --git a/tests/graphics/HwAccelerationTest/jni/native-lib.cpp b/tests/graphics/HwAccelerationTest/jni/native-lib.cpp index 407d4bf76336..2977c2157261 100644 --- a/tests/graphics/HwAccelerationTest/jni/native-lib.cpp +++ b/tests/graphics/HwAccelerationTest/jni/native-lib.cpp @@ -30,7 +30,7 @@ struct MyWrapper { void setBuffer(AHardwareBuffer* buffer) { ASurfaceTransaction* transaction = ASurfaceTransaction_create(); - ASurfaceTransaction_setBuffer(transaction, surfaceControl, buffer); + ASurfaceTransaction_setBuffer(transaction, surfaceControl, buffer, -1); ASurfaceTransaction_setVisibility(transaction, surfaceControl, ASURFACE_TRANSACTION_VISIBILITY_SHOW); ASurfaceTransaction_apply(transaction); diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt index 1381847c258f..ea2c8dacff50 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt @@ -39,8 +39,7 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.jar.JarOutputStream import java.util.zip.ZipEntry -import kotlin.math.abs -import kotlin.random.Random +import kotlin.math.absoluteValue import kotlin.system.exitProcess object ProtoLogTool { @@ -72,7 +71,11 @@ object ProtoLogTool { } private fun processClasses(command: CommandOptions) { - val generationHash = abs(Random.nextInt()) + // A deterministic hash based on the group jar path and the source files we are processing. + // The hash is required to make sure different ProtoLogImpls don't conflict. + val generationHash = (command.javaSourceArgs.toTypedArray() + command.protoLogGroupsJarArg) + .contentHashCode().absoluteValue + // Need to generate a new impl class to inject static constants into the class. val generatedProtoLogImplClass = "com.android.internal.protolog.ProtoLogImpl_$generationHash" diff --git a/tools/streaming_proto/Android.bp b/tools/streaming_proto/Android.bp index b18bdff7263f..b1b314fcdb19 100644 --- a/tools/streaming_proto/Android.bp +++ b/tools/streaming_proto/Android.bp @@ -17,6 +17,7 @@ // ========================================================== // Build the host executable: protoc-gen-javastream // ========================================================== + package { // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import @@ -41,6 +42,32 @@ cc_defaults { static_libs: ["libprotoc"], } +// ========================================================== +// Build the host static library: java_streaming_proto_lib +// ========================================================== + +cc_library_host_static { + name: "java_streaming_proto_lib", + defaults: ["protoc-gen-stream-defaults"], + target: { + darwin: { + cflags: ["-D_DARWIN_UNLIMITED_STREAMS"], + }, + }, + cflags: [ + "-Wno-format-y2k", + "-DSTATIC_ANDROIDFW_FOR_TOOLS", + ], + + srcs: [ + "java/java_proto_stream_code_generator.cpp", + ], +} + +// ========================================================== +// Build the host executable: protoc-gen-javastream +// ========================================================== + cc_binary_host { name: "protoc-gen-javastream", srcs: [ @@ -48,8 +75,13 @@ cc_binary_host { ], defaults: ["protoc-gen-stream-defaults"], + static_libs: ["java_streaming_proto_lib"], } +// ========================================================== +// Build the host executable: protoc-gen-cppstream +// ========================================================== + cc_binary_host { name: "protoc-gen-cppstream", srcs: [ @@ -60,13 +92,31 @@ cc_binary_host { } // ========================================================== +// Build the host tests: StreamingProtoTest +// ========================================================== + +cc_test_host { + name: "StreamingProtoTest", + defaults: ["protoc-gen-stream-defaults"], + srcs: [ + "test/unit/**/*.cpp", + ], + static_libs: [ + "java_streaming_proto_lib", + "libgmock", + "libgtest", + ], +} + +// ========================================================== // Build the java test // ========================================================== + java_library { - name: "StreamingProtoTest", + name: "StreamingProtoJavaIntegrationTest", srcs: [ - "test/**/*.java", - "test/**/*.proto", + "test/integration/**/*.java", + "test/integration/**/*.proto", ], proto: { type: "stream", diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.cpp b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp new file mode 100644 index 000000000000..9d61111fb5bd --- /dev/null +++ b/tools/streaming_proto/java/java_proto_stream_code_generator.cpp @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "java_proto_stream_code_generator.h" + +#include <stdio.h> + +#include <iomanip> +#include <iostream> +#include <map> +#include <sstream> +#include <string> + +#include "Errors.h" + +using namespace android::stream_proto; +using namespace google::protobuf::io; +using namespace std; + +/** + * If the descriptor gives us a class name, use that. Otherwise make one up from + * the filename of the .proto file. + */ +static string make_outer_class_name(const FileDescriptorProto& file_descriptor) { + string name = file_descriptor.options().java_outer_classname(); + if (name.size() == 0) { + name = to_camel_case(file_base_name(file_descriptor.name())); + if (name.size() == 0) { + ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE, + "Unable to make an outer class name for file: %s", + file_descriptor.name().c_str()); + name = "Unknown"; + } + } + return name; +} + +/** + * Figure out the package name that we are generating. + */ +static string make_java_package(const FileDescriptorProto& file_descriptor) { + if (file_descriptor.options().has_java_package()) { + return file_descriptor.options().java_package(); + } else { + return file_descriptor.package(); + } +} + +/** + * Figure out the name of the file we are generating. + */ +static string make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name) { + string const package = make_java_package(file_descriptor); + string result; + if (package.size() > 0) { + result = replace_string(package, '.', '/'); + result += '/'; + } + + result += class_name; + result += ".java"; + + return result; +} + +static string indent_more(const string& indent) { + return indent + INDENT; +} + +/** + * Write the constants for an enum. + */ +static void write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent) { + const int N = enu.value_size(); + text << indent << "// enum " << enu.name() << endl; + for (int i = 0; i < N; i++) { + const EnumValueDescriptorProto& value = enu.value(i); + text << indent << "public static final int " << make_constant_name(value.name()) << " = " + << value.number() << ";" << endl; + } + text << endl; +} + +/** + * Write a field. + */ +static void write_field(stringstream& text, const FieldDescriptorProto& field, + const string& indent) { + string optional_comment = + field.label() == FieldDescriptorProto::LABEL_OPTIONAL ? "optional " : ""; + string repeated_comment = + field.label() == FieldDescriptorProto::LABEL_REPEATED ? "repeated " : ""; + string proto_type = get_proto_type(field); + string packed_comment = field.options().packed() ? " [packed=true]" : ""; + text << indent << "// " << optional_comment << repeated_comment << proto_type << ' ' + << field.name() << " = " << field.number() << packed_comment << ';' << endl; + + text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x"; + + ios::fmtflags fmt(text.flags()); + text << setfill('0') << setw(16) << hex << get_field_id(field); + text.flags(fmt); + + text << "L;" << endl; + + text << endl; +} + +/** + * Write a Message constants class. + */ +static void write_message(stringstream& text, const DescriptorProto& message, + const string& indent) { + int N; + const string indented = indent_more(indent); + + text << indent << "// message " << message.name() << endl; + text << indent << "public final class " << message.name() << " {" << endl; + text << endl; + + // Enums + N = message.enum_type_size(); + for (int i = 0; i < N; i++) { + write_enum(text, message.enum_type(i), indented); + } + + // Nested classes + N = message.nested_type_size(); + for (int i = 0; i < N; i++) { + write_message(text, message.nested_type(i), indented); + } + + // Fields + N = message.field_size(); + for (int i = 0; i < N; i++) { + write_field(text, message.field(i), indented); + } + + text << indent << "}" << endl; + text << endl; +} + +/** + * Write the contents of a file. + * + * If there are enums and generate_outer is false, invalid java code will be generated. + */ +static void write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor, + const string& filename, bool generate_outer, + const vector<EnumDescriptorProto>& enums, + const vector<DescriptorProto>& messages) { + stringstream text; + + string const package_name = make_java_package(file_descriptor); + string const outer_class_name = make_outer_class_name(file_descriptor); + + text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl; + text << "// source: " << file_descriptor.name() << endl << endl; + + if (package_name.size() > 0) { + if (package_name.size() > 0) { + text << "package " << package_name << ";" << endl; + text << endl; + } + } + + // This bit of policy is android api rules specific: Raw proto classes + // must never be in the API + text << "/** @hide */" << endl; + // text << "@android.annotation.TestApi" << endl; + + if (generate_outer) { + text << "public final class " << outer_class_name << " {" << endl; + text << endl; + } + + size_t N; + const string indented = generate_outer ? indent_more("") : string(); + + N = enums.size(); + for (size_t i = 0; i < N; i++) { + write_enum(text, enums[i], indented); + } + + N = messages.size(); + for (size_t i = 0; i < N; i++) { + write_message(text, messages[i], indented); + } + + if (generate_outer) { + text << "}" << endl; + } + + CodeGeneratorResponse::File* file_response = response->add_file(); + file_response->set_name(filename); + file_response->set_content(text.str()); +} + +/** + * Write one file per class. Put all of the enums into the "outer" class. + */ +static void write_multiple_files(CodeGeneratorResponse* response, + const FileDescriptorProto& file_descriptor, + set<string> messages_to_compile) { + // If there is anything to put in the outer class file, create one + if (file_descriptor.enum_type_size() > 0) { + vector<EnumDescriptorProto> enums; + int N = file_descriptor.enum_type_size(); + for (int i = 0; i < N; i++) { + auto enum_full_name = + file_descriptor.package() + "." + file_descriptor.enum_type(i).name(); + if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) { + continue; + } + enums.push_back(file_descriptor.enum_type(i)); + } + + vector<DescriptorProto> messages; + + if (messages_to_compile.empty() || !enums.empty()) { + write_file(response, file_descriptor, + make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), + true, enums, messages); + } + } + + // For each of the message types, make a file + int N = file_descriptor.message_type_size(); + for (int i = 0; i < N; i++) { + vector<EnumDescriptorProto> enums; + + vector<DescriptorProto> messages; + + auto message_full_name = + file_descriptor.package() + "." + file_descriptor.message_type(i).name(); + if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) { + continue; + } + messages.push_back(file_descriptor.message_type(i)); + + if (messages_to_compile.empty() || !messages.empty()) { + write_file(response, file_descriptor, + make_file_name(file_descriptor, file_descriptor.message_type(i).name()), + false, enums, messages); + } + } +} + +static void write_single_file(CodeGeneratorResponse* response, + const FileDescriptorProto& file_descriptor, + set<string> messages_to_compile) { + int N; + + vector<EnumDescriptorProto> enums; + N = file_descriptor.enum_type_size(); + for (int i = 0; i < N; i++) { + auto enum_full_name = file_descriptor.package() + "." + file_descriptor.enum_type(i).name(); + if (!messages_to_compile.empty() && !messages_to_compile.count(enum_full_name)) { + continue; + } + + enums.push_back(file_descriptor.enum_type(i)); + } + + vector<DescriptorProto> messages; + N = file_descriptor.message_type_size(); + for (int i = 0; i < N; i++) { + auto message_full_name = + file_descriptor.package() + "." + file_descriptor.message_type(i).name(); + + if (!messages_to_compile.empty() && !messages_to_compile.count(message_full_name)) { + continue; + } + + messages.push_back(file_descriptor.message_type(i)); + } + + if (messages_to_compile.empty() || !enums.empty() || !messages.empty()) { + write_file(response, file_descriptor, + make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), true, + enums, messages); + } +} + +static void parse_args_string(stringstream args_string_stream, + set<string>* messages_to_compile_out) { + string line; + while (getline(args_string_stream, line, ';')) { + stringstream line_ss(line); + string arg_name; + getline(line_ss, arg_name, ':'); + if (arg_name == "include_filter") { + string full_message_name; + while (getline(line_ss, full_message_name, ',')) { + messages_to_compile_out->insert(full_message_name); + } + } else { + ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE, "Unexpected argument '%s'.", arg_name.c_str()); + } + } +} + +CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request) { + CodeGeneratorResponse response; + + set<string> messages_to_compile; + auto request_params = request.parameter(); + if (!request_params.empty()) { + parse_args_string(stringstream(request_params), &messages_to_compile); + } + + // Build the files we need. + const int N = request.proto_file_size(); + for (int i = 0; i < N; i++) { + const FileDescriptorProto& file_descriptor = request.proto_file(i); + if (should_generate_for_file(request, file_descriptor.name())) { + if (file_descriptor.options().java_multiple_files()) { + write_multiple_files(&response, file_descriptor, messages_to_compile); + } else { + write_single_file(&response, file_descriptor, messages_to_compile); + } + } + } + + return response; +} diff --git a/tools/streaming_proto/java/java_proto_stream_code_generator.h b/tools/streaming_proto/java/java_proto_stream_code_generator.h new file mode 100644 index 000000000000..d2492f75d383 --- /dev/null +++ b/tools/streaming_proto/java/java_proto_stream_code_generator.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H +#define AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H + +#include "stream_proto_utils.h" +#include "string_utils.h" + +using namespace android::stream_proto; +using namespace google::protobuf::io; +using namespace std; + +CodeGeneratorResponse generate_java_protostream_code(CodeGeneratorRequest request); + +#endif // AOSP_MAIN_FRAMEWORKS_BASE_JAVAPROTOSTREAMCODEGENERATOR_H
\ No newline at end of file diff --git a/tools/streaming_proto/java/main.cpp b/tools/streaming_proto/java/main.cpp index c9c50a561a04..5b35504865f8 100644 --- a/tools/streaming_proto/java/main.cpp +++ b/tools/streaming_proto/java/main.cpp @@ -1,268 +1,21 @@ -#include "Errors.h" -#include "stream_proto_utils.h" -#include "string_utils.h" - #include <stdio.h> + #include <iomanip> #include <iostream> -#include <sstream> #include <map> +#include <sstream> +#include <string> + +#include "Errors.h" +#include "java_proto_stream_code_generator.h" +#include "stream_proto_utils.h" using namespace android::stream_proto; using namespace google::protobuf::io; using namespace std; /** - * If the descriptor gives us a class name, use that. Otherwise make one up from - * the filename of the .proto file. - */ -static string -make_outer_class_name(const FileDescriptorProto& file_descriptor) -{ - string name = file_descriptor.options().java_outer_classname(); - if (name.size() == 0) { - name = to_camel_case(file_base_name(file_descriptor.name())); - if (name.size() == 0) { - ERRORS.Add(UNKNOWN_FILE, UNKNOWN_LINE, - "Unable to make an outer class name for file: %s", - file_descriptor.name().c_str()); - name = "Unknown"; - } - } - return name; -} - -/** - * Figure out the package name that we are generating. - */ -static string -make_java_package(const FileDescriptorProto& file_descriptor) { - if (file_descriptor.options().has_java_package()) { - return file_descriptor.options().java_package(); - } else { - return file_descriptor.package(); - } -} - -/** - * Figure out the name of the file we are generating. - */ -static string -make_file_name(const FileDescriptorProto& file_descriptor, const string& class_name) -{ - string const package = make_java_package(file_descriptor); - string result; - if (package.size() > 0) { - result = replace_string(package, '.', '/'); - result += '/'; - } - - result += class_name; - result += ".java"; - - return result; -} - -static string -indent_more(const string& indent) -{ - return indent + INDENT; -} - -/** - * Write the constants for an enum. - */ -static void -write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& indent) -{ - const int N = enu.value_size(); - text << indent << "// enum " << enu.name() << endl; - for (int i=0; i<N; i++) { - const EnumValueDescriptorProto& value = enu.value(i); - text << indent << "public static final int " - << make_constant_name(value.name()) - << " = " << value.number() << ";" << endl; - } - text << endl; -} - -/** - * Write a field. - */ -static void -write_field(stringstream& text, const FieldDescriptorProto& field, const string& indent) -{ - string optional_comment = field.label() == FieldDescriptorProto::LABEL_OPTIONAL - ? "optional " : ""; - string repeated_comment = field.label() == FieldDescriptorProto::LABEL_REPEATED - ? "repeated " : ""; - string proto_type = get_proto_type(field); - string packed_comment = field.options().packed() - ? " [packed=true]" : ""; - text << indent << "// " << optional_comment << repeated_comment << proto_type << ' ' - << field.name() << " = " << field.number() << packed_comment << ';' << endl; - - text << indent << "public static final long " << make_constant_name(field.name()) << " = 0x"; - - ios::fmtflags fmt(text.flags()); - text << setfill('0') << setw(16) << hex << get_field_id(field); - text.flags(fmt); - - text << "L;" << endl; - - text << endl; -} - -/** - * Write a Message constants class. - */ -static void -write_message(stringstream& text, const DescriptorProto& message, const string& indent) -{ - int N; - const string indented = indent_more(indent); - - text << indent << "// message " << message.name() << endl; - text << indent << "public final class " << message.name() << " {" << endl; - text << endl; - - // Enums - N = message.enum_type_size(); - for (int i=0; i<N; i++) { - write_enum(text, message.enum_type(i), indented); - } - - // Nested classes - N = message.nested_type_size(); - for (int i=0; i<N; i++) { - write_message(text, message.nested_type(i), indented); - } - - // Fields - N = message.field_size(); - for (int i=0; i<N; i++) { - write_field(text, message.field(i), indented); - } - - text << indent << "}" << endl; - text << endl; -} - -/** - * Write the contents of a file. * - * If there are enums and generate_outer is false, invalid java code will be generated. - */ -static void -write_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor, - const string& filename, bool generate_outer, - const vector<EnumDescriptorProto>& enums, const vector<DescriptorProto>& messages) -{ - stringstream text; - - string const package_name = make_java_package(file_descriptor); - string const outer_class_name = make_outer_class_name(file_descriptor); - - text << "// Generated by protoc-gen-javastream. DO NOT MODIFY." << endl; - text << "// source: " << file_descriptor.name() << endl << endl; - - if (package_name.size() > 0) { - if (package_name.size() > 0) { - text << "package " << package_name << ";" << endl; - text << endl; - } - } - - // This bit of policy is android api rules specific: Raw proto classes - // must never be in the API - text << "/** @hide */" << endl; -// text << "@android.annotation.TestApi" << endl; - - if (generate_outer) { - text << "public final class " << outer_class_name << " {" << endl; - text << endl; - } - - size_t N; - const string indented = generate_outer ? indent_more("") : string(); - - N = enums.size(); - for (size_t i=0; i<N; i++) { - write_enum(text, enums[i], indented); - } - - N = messages.size(); - for (size_t i=0; i<N; i++) { - write_message(text, messages[i], indented); - } - - if (generate_outer) { - text << "}" << endl; - } - - CodeGeneratorResponse::File* file_response = response->add_file(); - file_response->set_name(filename); - file_response->set_content(text.str()); -} - -/** - * Write one file per class. Put all of the enums into the "outer" class. - */ -static void -write_multiple_files(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor) -{ - // If there is anything to put in the outer class file, create one - if (file_descriptor.enum_type_size() > 0) { - vector<EnumDescriptorProto> enums; - int N = file_descriptor.enum_type_size(); - for (int i=0; i<N; i++) { - enums.push_back(file_descriptor.enum_type(i)); - } - - vector<DescriptorProto> messages; - - write_file(response, file_descriptor, - make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), - true, enums, messages); - } - - // For each of the message types, make a file - int N = file_descriptor.message_type_size(); - for (int i=0; i<N; i++) { - vector<EnumDescriptorProto> enums; - - vector<DescriptorProto> messages; - messages.push_back(file_descriptor.message_type(i)); - - write_file(response, file_descriptor, - make_file_name(file_descriptor, file_descriptor.message_type(i).name()), - false, enums, messages); - } -} - -static void -write_single_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor) -{ - int N; - - vector<EnumDescriptorProto> enums; - N = file_descriptor.enum_type_size(); - for (int i=0; i<N; i++) { - enums.push_back(file_descriptor.enum_type(i)); - } - - vector<DescriptorProto> messages; - N = file_descriptor.message_type_size(); - for (int i=0; i<N; i++) { - messages.push_back(file_descriptor.message_type(i)); - } - - write_file(response, file_descriptor, - make_file_name(file_descriptor, make_outer_class_name(file_descriptor)), - true, enums, messages); -} - -/** * Main. */ int @@ -273,24 +26,11 @@ main(int argc, char const*const* argv) GOOGLE_PROTOBUF_VERIFY_VERSION; - CodeGeneratorRequest request; - CodeGeneratorResponse response; - // Read the request + CodeGeneratorRequest request; request.ParseFromIstream(&cin); - // Build the files we need. - const int N = request.proto_file_size(); - for (int i=0; i<N; i++) { - const FileDescriptorProto& file_descriptor = request.proto_file(i); - if (should_generate_for_file(request, file_descriptor.name())) { - if (file_descriptor.options().java_multiple_files()) { - write_multiple_files(&response, file_descriptor); - } else { - write_single_file(&response, file_descriptor); - } - } - } + CodeGeneratorResponse response = generate_java_protostream_code(request); // If we had errors, don't write the response. Print the errors and exit. if (ERRORS.HasErrors()) { diff --git a/tools/streaming_proto/test/imported.proto b/tools/streaming_proto/test/integration/imported.proto index 05c8f0c54fac..05c8f0c54fac 100644 --- a/tools/streaming_proto/test/imported.proto +++ b/tools/streaming_proto/test/integration/imported.proto diff --git a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java index 838e41ee1c08..2a7001b294ea 100644 --- a/core/java/android/hardware/CameraPrivacyAllowlistEntry.aidl +++ b/tools/streaming_proto/test/integration/src/com/android/streaming_proto_test/Main.java @@ -1,11 +1,11 @@ -/** - * Copyright (c) 2024, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,11 +14,10 @@ * limitations under the License. */ -package android.hardware; +package com.android.streaming_proto_test; -/** @hide */ -parcelable CameraPrivacyAllowlistEntry { - String packageName; - boolean isMandatory; +public class Main { + public void main(String[] argv) { + System.out.println("hello world"); + } } - diff --git a/tools/streaming_proto/test/test.proto b/tools/streaming_proto/test/integration/test.proto index de80ed6612fc..3cf81b4ffd56 100644 --- a/tools/streaming_proto/test/test.proto +++ b/tools/streaming_proto/test/integration/test.proto @@ -16,7 +16,7 @@ syntax = "proto2"; -import "frameworks/base/tools/streaming_proto/test/imported.proto"; +import "frameworks/base/tools/streaming_proto/test/integration/imported.proto"; package com.android.streaming_proto_test; diff --git a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java b/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java deleted file mode 100644 index 1246f539b44b..000000000000 --- a/tools/streaming_proto/test/src/com/android/streaming_proto_test/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.streaming_proto_test; - -public class Main { - public void main(String[] argv) { - System.out.println("hello world"); - } -} diff --git a/tools/streaming_proto/test/unit/streaming_proto_java.cpp b/tools/streaming_proto/test/unit/streaming_proto_java.cpp new file mode 100644 index 000000000000..8df9716b312d --- /dev/null +++ b/tools/streaming_proto/test/unit/streaming_proto_java.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "java/java_proto_stream_code_generator.h" + +using ::testing::HasSubstr; +using ::testing::Not; + +static void add_my_test_proto_file(CodeGeneratorRequest* request) { + request->add_file_to_generate("MyTestProtoFile"); + + FileDescriptorProto* file_desc = request->add_proto_file(); + file_desc->set_name("MyTestProtoFile"); + file_desc->set_package("test.package"); + + auto* file_options = file_desc->mutable_options(); + file_options->set_java_multiple_files(false); + + auto* message = file_desc->add_message_type(); + message->set_name("MyTestMessage"); + + auto* field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("my_test_field"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("my_other_test_field"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("my_other_test_message"); +} + +static void add_my_other_test_proto_file(CodeGeneratorRequest* request) { + request->add_file_to_generate("MyOtherTestProtoFile"); + + FileDescriptorProto* file_desc = request->add_proto_file(); + file_desc->set_name("MyOtherTestProtoFile"); + file_desc->set_package("test.package"); + + auto* file_options = file_desc->mutable_options(); + file_options->set_java_multiple_files(false); + + auto* message = file_desc->add_message_type(); + message->set_name("MyOtherTestMessage"); + + auto* field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("a_test_field"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("another_test_field"); +} + +static CodeGeneratorRequest create_simple_two_file_request() { + CodeGeneratorRequest request; + + add_my_test_proto_file(&request); + add_my_other_test_proto_file(&request); + + return request; +} + +static CodeGeneratorRequest create_simple_multi_file_request() { + CodeGeneratorRequest request; + + request.add_file_to_generate("MyMultiMessageTestProtoFile"); + + FileDescriptorProto* file_desc = request.add_proto_file(); + file_desc->set_name("MyMultiMessageTestProtoFile"); + file_desc->set_package("test.package"); + + auto* file_options = file_desc->mutable_options(); + file_options->set_java_multiple_files(true); + + auto* message = file_desc->add_message_type(); + message->set_name("MyTestMessage"); + + auto* field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("my_test_field"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("my_other_test_field"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("my_other_test_message"); + + message = file_desc->add_message_type(); + message->set_name("MyOtherTestMessage"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("a_test_field"); + + field = message->add_field(); + field->set_label(FieldDescriptorProto::LABEL_OPTIONAL); + field->set_name("another_test_field"); + + return request; +} + +TEST(StreamingProtoJavaTest, NoFilter) { + CodeGeneratorRequest request = create_simple_two_file_request(); + CodeGeneratorResponse response = generate_java_protostream_code(request); + + auto generated_file_count = response.file_size(); + EXPECT_EQ(generated_file_count, 2); + + EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java"); + EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile")); + EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD")); + + EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestProtoFile.java"); + EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestProtoFile")); + EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage")); + EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD")); + EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD")); +} + +TEST(StreamingProtoJavaTest, WithFilter) { + CodeGeneratorRequest request = create_simple_two_file_request(); + request.set_parameter("include_filter:test.package.MyTestMessage"); + CodeGeneratorResponse response = generate_java_protostream_code(request); + + auto generated_file_count = response.file_size(); + EXPECT_EQ(generated_file_count, 1); + + EXPECT_EQ(response.file(0).name(), "test/package/MyTestProtoFile.java"); + EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestProtoFile")); + EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD")); +} + +TEST(StreamingProtoJavaTest, WithoutFilter_MultipleJavaFiles) { + CodeGeneratorRequest request = create_simple_multi_file_request(); + CodeGeneratorResponse response = generate_java_protostream_code(request); + + auto generated_file_count = response.file_size(); + EXPECT_EQ(generated_file_count, 2); + + EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java"); + EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile"))); + EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD")); + + EXPECT_EQ(response.file(1).name(), "test/package/MyOtherTestMessage.java"); + EXPECT_THAT(response.file(1).content(), Not(HasSubstr("class MyOtherTestProtoFile"))); + EXPECT_THAT(response.file(1).content(), HasSubstr("class MyOtherTestMessage")); + EXPECT_THAT(response.file(1).content(), HasSubstr("long A_TEST_FIELD")); + EXPECT_THAT(response.file(1).content(), HasSubstr("long ANOTHER_TEST_FIELD")); +} + +TEST(StreamingProtoJavaTest, WithFilter_MultipleJavaFiles) { + CodeGeneratorRequest request = create_simple_multi_file_request(); + request.set_parameter("include_filter:test.package.MyTestMessage"); + CodeGeneratorResponse response = generate_java_protostream_code(request); + + auto generated_file_count = response.file_size(); + EXPECT_EQ(generated_file_count, 1); + + EXPECT_EQ(response.file(0).name(), "test/package/MyTestMessage.java"); + EXPECT_THAT(response.file(0).content(), Not(HasSubstr("class MyTestProtoFile"))); + EXPECT_THAT(response.file(0).content(), HasSubstr("class MyTestMessage")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_TEST_FIELD")); + EXPECT_THAT(response.file(0).content(), HasSubstr("long MY_OTHER_TEST_FIELD")); +} diff --git a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java index 58638e8e1af4..45ab9863ff73 100644 --- a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java +++ b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java @@ -718,6 +718,9 @@ public class WifiNl80211Manager { } catch (RemoteException e1) { Log.e(TAG, "Failed to get IClientInterface due to remote exception"); return false; + } catch (NullPointerException e2) { + Log.e(TAG, "setupInterfaceForClientMode NullPointerException"); + return false; } if (clientInterface == null) { @@ -785,6 +788,9 @@ public class WifiNl80211Manager { } catch (RemoteException e1) { Log.e(TAG, "Failed to teardown client interface due to remote exception"); return false; + } catch (NullPointerException e2) { + Log.e(TAG, "tearDownClientInterface NullPointerException"); + return false; } if (!success) { Log.e(TAG, "Failed to teardown client interface"); @@ -816,6 +822,9 @@ public class WifiNl80211Manager { } catch (RemoteException e1) { Log.e(TAG, "Failed to get IApInterface due to remote exception"); return false; + } catch (NullPointerException e2) { + Log.e(TAG, "setupInterfaceForSoftApMode NullPointerException"); + return false; } if (apInterface == null) { @@ -854,6 +863,9 @@ public class WifiNl80211Manager { } catch (RemoteException e1) { Log.e(TAG, "Failed to teardown AP interface due to remote exception"); return false; + } catch (NullPointerException e2) { + Log.e(TAG, "tearDownSoftApInterface NullPointerException"); + return false; } if (!success) { Log.e(TAG, "Failed to teardown AP interface"); @@ -1328,6 +1340,8 @@ public class WifiNl80211Manager { } } catch (RemoteException e1) { Log.e(TAG, "Failed to request getChannelsForBand due to remote exception"); + } catch (NullPointerException e2) { + Log.e(TAG, "getChannelsMhzForBand NullPointerException"); } if (result == null) { result = new int[0]; @@ -1352,7 +1366,8 @@ public class WifiNl80211Manager { */ @Nullable public DeviceWiphyCapabilities getDeviceWiphyCapabilities(@NonNull String ifaceName) { if (mWificond == null) { - Log.e(TAG, "getDeviceWiphyCapabilities: mWificond binder is null! Did wificond die?"); + Log.e(TAG, "getDeviceWiphyCapabilities: mWificond binder is null! " + + "Did wificond die?"); return null; } @@ -1360,6 +1375,9 @@ public class WifiNl80211Manager { return mWificond.getDeviceWiphyCapabilities(ifaceName); } catch (RemoteException e) { return null; + } catch (NullPointerException e2) { + Log.e(TAG, "getDeviceWiphyCapabilities NullPointerException"); + return null; } } @@ -1409,6 +1427,8 @@ public class WifiNl80211Manager { Log.i(TAG, "Receive country code change to " + newCountryCode); } catch (RemoteException re) { re.rethrowFromSystemServer(); + } catch (NullPointerException e) { + new RemoteException("Wificond service doesn't exist!").rethrowFromSystemServer(); } } |